1use async_trait::async_trait;
13use chrono::{DateTime, Utc};
14use http::{Method, StatusCode};
15use serde_json::{json, Value};
16use std::sync::Arc;
17use tokio::sync::Mutex as AsyncMutex;
18
19use fakecloud_core::pagination::paginate_checked;
20use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
21use fakecloud_persistence::SnapshotStore;
22
23use crate::state::{
24 access_entry_arn, addon_arn, capability_arn, cluster_arn, eks_anywhere_subscription_arn,
25 fargate_profile_arn, identity_provider_config_arn, nodegroup_arn, pod_identity_association_arn,
26 AccessEntry, Addon, AssociatedPolicy, Capability, Cluster, EksAnywhereSubscription,
27 EksSnapshot, FargateProfile, IdentityProviderConfig, Insight, InsightsRefresh, Nodegroup,
28 PodIdentityAssociation, SharedEksState, Update, DEFAULT_K8S_VERSION,
29 EKS_SNAPSHOT_SCHEMA_VERSION,
30};
31
32const LOG_TYPES: &[&str] = &[
34 "api",
35 "audit",
36 "authenticator",
37 "controllerManager",
38 "scheduler",
39];
40
41pub const EKS_ACTIONS: &[&str] = &[
42 "CreateCluster",
43 "DescribeCluster",
44 "ListClusters",
45 "DeleteCluster",
46 "UpdateClusterConfig",
47 "UpdateClusterVersion",
48 "DescribeUpdate",
49 "ListUpdates",
50 "TagResource",
51 "UntagResource",
52 "ListTagsForResource",
53 "CreateNodegroup",
54 "DescribeNodegroup",
55 "ListNodegroups",
56 "DeleteNodegroup",
57 "UpdateNodegroupConfig",
58 "UpdateNodegroupVersion",
59 "CreateFargateProfile",
60 "DescribeFargateProfile",
61 "ListFargateProfiles",
62 "DeleteFargateProfile",
63 "CreateAddon",
64 "DescribeAddon",
65 "ListAddons",
66 "DeleteAddon",
67 "UpdateAddon",
68 "DescribeAddonVersions",
69 "DescribeAddonConfiguration",
70 "CreateAccessEntry",
71 "DescribeAccessEntry",
72 "ListAccessEntries",
73 "DeleteAccessEntry",
74 "UpdateAccessEntry",
75 "AssociateAccessPolicy",
76 "DisassociateAccessPolicy",
77 "ListAssociatedAccessPolicies",
78 "ListAccessPolicies",
79 "AssociateIdentityProviderConfig",
80 "DisassociateIdentityProviderConfig",
81 "DescribeIdentityProviderConfig",
82 "ListIdentityProviderConfigs",
83 "CreatePodIdentityAssociation",
84 "DeletePodIdentityAssociation",
85 "DescribePodIdentityAssociation",
86 "ListPodIdentityAssociations",
87 "UpdatePodIdentityAssociation",
88 "DescribeInsight",
89 "ListInsights",
90 "DescribeInsightsRefresh",
91 "StartInsightsRefresh",
92 "AssociateEncryptionConfig",
93 "CancelUpdate",
94 "DeregisterCluster",
95 "RegisterCluster",
96 "DescribeClusterVersions",
97 "CreateCapability",
98 "DeleteCapability",
99 "DescribeCapability",
100 "ListCapabilities",
101 "UpdateCapability",
102 "CreateEksAnywhereSubscription",
103 "DeleteEksAnywhereSubscription",
104 "DescribeEksAnywhereSubscription",
105 "ListEksAnywhereSubscriptions",
106 "UpdateEksAnywhereSubscription",
107];
108
109pub struct EksService {
110 state: SharedEksState,
111 snapshot_store: Option<Arc<dyn SnapshotStore>>,
112 snapshot_lock: Arc<AsyncMutex<()>>,
113}
114
115enum PathArgs {
116 None,
117 Name(String),
118 Update {
119 name: String,
120 update_id: String,
121 },
122 Arn(String),
123 Cluster(String),
125 ClusterChild {
127 cluster: String,
128 name: String,
129 },
130 AccessPolicyChild {
133 cluster: String,
134 principal: String,
135 policy_arn: String,
136 },
137}
138
139impl EksService {
140 pub fn new(state: SharedEksState) -> Self {
141 Self {
142 state,
143 snapshot_store: None,
144 snapshot_lock: Arc::new(AsyncMutex::new(())),
145 }
146 }
147
148 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
149 self.snapshot_store = Some(store);
150 self
151 }
152
153 async fn save_snapshot(&self) {
154 save_eks_snapshot(
155 &self.state,
156 self.snapshot_store.clone(),
157 &self.snapshot_lock,
158 )
159 .await;
160 }
161
162 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
164 let store = self.snapshot_store.clone()?;
165 let state = self.state.clone();
166 let lock = self.snapshot_lock.clone();
167 Some(Arc::new(move || {
168 let state = state.clone();
169 let store = store.clone();
170 let lock = lock.clone();
171 Box::pin(async move {
172 save_eks_snapshot(&state, Some(store), &lock).await;
173 })
174 }))
175 }
176
177 fn resolve_action(req: &AwsRequest) -> Option<(&'static str, PathArgs)> {
178 let raw = req.raw_path.split('?').next().unwrap_or(&req.raw_path);
187 let trimmed = raw.strip_prefix('/').unwrap_or(raw);
188 let trimmed = trimmed.strip_suffix('/').unwrap_or(trimmed);
189 let segs: Vec<&str> = if trimmed.is_empty() {
190 Vec::new()
191 } else {
192 trimmed.split('/').collect()
193 };
194 match (&req.method, segs.as_slice()) {
195 (&Method::POST, ["clusters"]) => Some(("CreateCluster", PathArgs::None)),
196 (&Method::GET, ["clusters"]) => Some(("ListClusters", PathArgs::None)),
197 (&Method::GET, ["clusters", name]) => {
198 Some(("DescribeCluster", PathArgs::Name(decode(name))))
199 }
200 (&Method::DELETE, ["clusters", name]) => {
201 Some(("DeleteCluster", PathArgs::Name(decode(name))))
202 }
203 (&Method::POST, ["clusters", name, "update-config"]) => {
204 Some(("UpdateClusterConfig", PathArgs::Name(decode(name))))
205 }
206 (&Method::POST, ["clusters", name, "updates"]) => {
207 Some(("UpdateClusterVersion", PathArgs::Name(decode(name))))
208 }
209 (&Method::GET, ["clusters", name, "updates"]) => {
210 Some(("ListUpdates", PathArgs::Name(decode(name))))
211 }
212 (&Method::GET, ["clusters", name, "updates", update_id]) => Some((
213 "DescribeUpdate",
214 PathArgs::Update {
215 name: decode(name),
216 update_id: decode(update_id),
217 },
218 )),
219 (&Method::POST, ["clusters", c, "node-groups"]) => {
221 Some(("CreateNodegroup", PathArgs::Cluster(decode(c))))
222 }
223 (&Method::GET, ["clusters", c, "node-groups"]) => {
224 Some(("ListNodegroups", PathArgs::Cluster(decode(c))))
225 }
226 (&Method::GET, ["clusters", c, "node-groups", n]) => Some((
227 "DescribeNodegroup",
228 PathArgs::ClusterChild {
229 cluster: decode(c),
230 name: decode(n),
231 },
232 )),
233 (&Method::DELETE, ["clusters", c, "node-groups", n]) => Some((
234 "DeleteNodegroup",
235 PathArgs::ClusterChild {
236 cluster: decode(c),
237 name: decode(n),
238 },
239 )),
240 (&Method::POST, ["clusters", c, "node-groups", n, "update-config"]) => Some((
241 "UpdateNodegroupConfig",
242 PathArgs::ClusterChild {
243 cluster: decode(c),
244 name: decode(n),
245 },
246 )),
247 (&Method::POST, ["clusters", c, "node-groups", n, "update-version"]) => Some((
248 "UpdateNodegroupVersion",
249 PathArgs::ClusterChild {
250 cluster: decode(c),
251 name: decode(n),
252 },
253 )),
254 (&Method::POST, ["clusters", c, "fargate-profiles"]) => {
256 Some(("CreateFargateProfile", PathArgs::Cluster(decode(c))))
257 }
258 (&Method::GET, ["clusters", c, "fargate-profiles"]) => {
259 Some(("ListFargateProfiles", PathArgs::Cluster(decode(c))))
260 }
261 (&Method::GET, ["clusters", c, "fargate-profiles", n]) => Some((
262 "DescribeFargateProfile",
263 PathArgs::ClusterChild {
264 cluster: decode(c),
265 name: decode(n),
266 },
267 )),
268 (&Method::DELETE, ["clusters", c, "fargate-profiles", n]) => Some((
269 "DeleteFargateProfile",
270 PathArgs::ClusterChild {
271 cluster: decode(c),
272 name: decode(n),
273 },
274 )),
275 (&Method::POST, ["clusters", c, "addons"]) => {
277 Some(("CreateAddon", PathArgs::Cluster(decode(c))))
278 }
279 (&Method::GET, ["clusters", c, "addons"]) => {
280 Some(("ListAddons", PathArgs::Cluster(decode(c))))
281 }
282 (&Method::GET, ["clusters", c, "addons", n]) => Some((
283 "DescribeAddon",
284 PathArgs::ClusterChild {
285 cluster: decode(c),
286 name: decode(n),
287 },
288 )),
289 (&Method::DELETE, ["clusters", c, "addons", n]) => Some((
290 "DeleteAddon",
291 PathArgs::ClusterChild {
292 cluster: decode(c),
293 name: decode(n),
294 },
295 )),
296 (&Method::POST, ["clusters", c, "addons", n, "update"]) => Some((
297 "UpdateAddon",
298 PathArgs::ClusterChild {
299 cluster: decode(c),
300 name: decode(n),
301 },
302 )),
303 (&Method::GET, ["addons", "supported-versions"]) => {
305 Some(("DescribeAddonVersions", PathArgs::None))
306 }
307 (&Method::GET, ["addons", "configuration-schemas"]) => {
308 Some(("DescribeAddonConfiguration", PathArgs::None))
309 }
310 (&Method::POST, ["clusters", c, "access-entries"]) => {
312 Some(("CreateAccessEntry", PathArgs::Cluster(decode(c))))
313 }
314 (&Method::GET, ["clusters", c, "access-entries"]) => {
315 Some(("ListAccessEntries", PathArgs::Cluster(decode(c))))
316 }
317 (&Method::GET, ["clusters", c, "access-entries", p]) => Some((
318 "DescribeAccessEntry",
319 PathArgs::ClusterChild {
320 cluster: decode(c),
321 name: decode(p),
322 },
323 )),
324 (&Method::DELETE, ["clusters", c, "access-entries", p]) => Some((
325 "DeleteAccessEntry",
326 PathArgs::ClusterChild {
327 cluster: decode(c),
328 name: decode(p),
329 },
330 )),
331 (&Method::POST, ["clusters", c, "access-entries", p]) => Some((
332 "UpdateAccessEntry",
333 PathArgs::ClusterChild {
334 cluster: decode(c),
335 name: decode(p),
336 },
337 )),
338 (&Method::POST, ["clusters", c, "access-entries", p, "access-policies"]) => Some((
340 "AssociateAccessPolicy",
341 PathArgs::ClusterChild {
342 cluster: decode(c),
343 name: decode(p),
344 },
345 )),
346 (&Method::GET, ["clusters", c, "access-entries", p, "access-policies"]) => Some((
347 "ListAssociatedAccessPolicies",
348 PathArgs::ClusterChild {
349 cluster: decode(c),
350 name: decode(p),
351 },
352 )),
353 (&Method::DELETE, ["clusters", c, "access-entries", p, "access-policies", policy]) => {
354 Some((
355 "DisassociateAccessPolicy",
356 PathArgs::AccessPolicyChild {
357 cluster: decode(c),
358 principal: decode(p),
359 policy_arn: decode(policy),
360 },
361 ))
362 }
363 (&Method::GET, ["access-policies"]) => Some(("ListAccessPolicies", PathArgs::None)),
365 (&Method::POST, ["clusters", c, "identity-provider-configs", "associate"]) => Some((
369 "AssociateIdentityProviderConfig",
370 PathArgs::Cluster(decode(c)),
371 )),
372 (&Method::POST, ["clusters", c, "identity-provider-configs", "disassociate"]) => {
373 Some((
374 "DisassociateIdentityProviderConfig",
375 PathArgs::Cluster(decode(c)),
376 ))
377 }
378 (&Method::POST, ["clusters", c, "identity-provider-configs", "describe"]) => Some((
379 "DescribeIdentityProviderConfig",
380 PathArgs::Cluster(decode(c)),
381 )),
382 (&Method::GET, ["clusters", c, "identity-provider-configs"]) => {
383 Some(("ListIdentityProviderConfigs", PathArgs::Cluster(decode(c))))
384 }
385 (&Method::POST, ["clusters", c, "pod-identity-associations"]) => {
387 Some(("CreatePodIdentityAssociation", PathArgs::Cluster(decode(c))))
388 }
389 (&Method::GET, ["clusters", c, "pod-identity-associations"]) => {
390 Some(("ListPodIdentityAssociations", PathArgs::Cluster(decode(c))))
391 }
392 (&Method::GET, ["clusters", c, "pod-identity-associations", id]) => Some((
393 "DescribePodIdentityAssociation",
394 PathArgs::ClusterChild {
395 cluster: decode(c),
396 name: decode(id),
397 },
398 )),
399 (&Method::DELETE, ["clusters", c, "pod-identity-associations", id]) => Some((
400 "DeletePodIdentityAssociation",
401 PathArgs::ClusterChild {
402 cluster: decode(c),
403 name: decode(id),
404 },
405 )),
406 (&Method::POST, ["clusters", c, "pod-identity-associations", id]) => Some((
407 "UpdatePodIdentityAssociation",
408 PathArgs::ClusterChild {
409 cluster: decode(c),
410 name: decode(id),
411 },
412 )),
413 (&Method::POST, ["clusters", c, "insights"]) => {
416 Some(("ListInsights", PathArgs::Cluster(decode(c))))
417 }
418 (&Method::GET, ["clusters", c, "insights", id]) => Some((
419 "DescribeInsight",
420 PathArgs::ClusterChild {
421 cluster: decode(c),
422 name: decode(id),
423 },
424 )),
425 (&Method::GET, ["clusters", c, "insights-refresh"]) => {
426 Some(("DescribeInsightsRefresh", PathArgs::Cluster(decode(c))))
427 }
428 (&Method::POST, ["clusters", c, "insights-refresh"]) => {
429 Some(("StartInsightsRefresh", PathArgs::Cluster(decode(c))))
430 }
431 (&Method::POST, ["clusters", c, "encryption-config", "associate"]) => {
433 Some(("AssociateEncryptionConfig", PathArgs::Cluster(decode(c))))
434 }
435 (&Method::POST, ["clusters", name, "updates", update_id, "cancel-update"]) => Some((
437 "CancelUpdate",
438 PathArgs::Update {
439 name: decode(name),
440 update_id: decode(update_id),
441 },
442 )),
443 (&Method::POST, ["cluster-registrations"]) => Some(("RegisterCluster", PathArgs::None)),
445 (&Method::DELETE, ["cluster-registrations", name]) => {
446 Some(("DeregisterCluster", PathArgs::Name(decode(name))))
447 }
448 (&Method::GET, ["cluster-versions"]) => {
450 Some(("DescribeClusterVersions", PathArgs::None))
451 }
452 (&Method::POST, ["clusters", c, "capabilities"]) => {
454 Some(("CreateCapability", PathArgs::Cluster(decode(c))))
455 }
456 (&Method::GET, ["clusters", c, "capabilities"]) => {
457 Some(("ListCapabilities", PathArgs::Cluster(decode(c))))
458 }
459 (&Method::GET, ["clusters", c, "capabilities", n]) => Some((
460 "DescribeCapability",
461 PathArgs::ClusterChild {
462 cluster: decode(c),
463 name: decode(n),
464 },
465 )),
466 (&Method::DELETE, ["clusters", c, "capabilities", n]) => Some((
467 "DeleteCapability",
468 PathArgs::ClusterChild {
469 cluster: decode(c),
470 name: decode(n),
471 },
472 )),
473 (&Method::POST, ["clusters", c, "capabilities", n]) => Some((
474 "UpdateCapability",
475 PathArgs::ClusterChild {
476 cluster: decode(c),
477 name: decode(n),
478 },
479 )),
480 (&Method::POST, ["eks-anywhere-subscriptions"]) => {
482 Some(("CreateEksAnywhereSubscription", PathArgs::None))
483 }
484 (&Method::GET, ["eks-anywhere-subscriptions"]) => {
485 Some(("ListEksAnywhereSubscriptions", PathArgs::None))
486 }
487 (&Method::GET, ["eks-anywhere-subscriptions", id]) => Some((
488 "DescribeEksAnywhereSubscription",
489 PathArgs::Name(decode(id)),
490 )),
491 (&Method::DELETE, ["eks-anywhere-subscriptions", id]) => {
492 Some(("DeleteEksAnywhereSubscription", PathArgs::Name(decode(id))))
493 }
494 (&Method::POST, ["eks-anywhere-subscriptions", id]) => {
495 Some(("UpdateEksAnywhereSubscription", PathArgs::Name(decode(id))))
496 }
497 (&Method::POST, ["tags", arn]) => Some(("TagResource", PathArgs::Arn(decode(arn)))),
498 (&Method::DELETE, ["tags", arn]) => Some(("UntagResource", PathArgs::Arn(decode(arn)))),
499 (&Method::GET, ["tags", arn]) => {
500 Some(("ListTagsForResource", PathArgs::Arn(decode(arn))))
501 }
502 _ => None,
503 }
504 }
505
506 fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
507 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
508
509 let name = body
510 .get("name")
511 .and_then(|v| v.as_str())
512 .ok_or_else(|| invalid_parameter("name is required"))?
513 .to_string();
514 validate_cluster_name(&name)?;
515
516 let role_arn = body
517 .get("roleArn")
518 .and_then(|v| v.as_str())
519 .ok_or_else(|| invalid_parameter("roleArn is required"))?
520 .to_string();
521
522 let vpc_req = body
523 .get("resourcesVpcConfig")
524 .filter(|v| v.is_object())
525 .ok_or_else(|| invalid_parameter("resourcesVpcConfig is required"))?;
526
527 let version = body
528 .get("version")
529 .and_then(|v| v.as_str())
530 .unwrap_or(DEFAULT_K8S_VERSION)
531 .to_string();
532
533 let mut accounts = self.state.write();
534 let state = accounts.get_or_create(&req.account_id);
535 if state.clusters.contains_key(&name) {
536 return Err(AwsServiceError::aws_error(
537 StatusCode::CONFLICT,
538 "ResourceInUseException",
539 format!("Cluster already exists with name: {name}"),
540 ));
541 }
542
543 let region = req.region.clone();
544 let account_id = req.account_id.clone();
545 let arn = cluster_arn(®ion, &account_id, &name);
546 let id = uuid::Uuid::new_v4().to_string();
547
548 let tags = parse_tag_map(body.get("tags"));
549
550 let cluster = Cluster {
551 name: name.clone(),
552 arn: arn.clone(),
553 version,
554 role_arn,
555 status: "CREATING".to_string(),
556 created_at: Utc::now(),
557 endpoint: format!(
558 "https://{}.gr7.{region}.eks.amazonaws.com",
559 id.replace('-', "").to_uppercase()
560 ),
561 platform_version: "eks.1".to_string(),
562 certificate_authority_data: default_ca_data(),
563 resources_vpc_config: build_vpc_config_response(vpc_req, &id),
564 kubernetes_network_config: build_k8s_network_config(
565 body.get("kubernetesNetworkConfig"),
566 ),
567 logging: build_logging(body.get("logging")),
568 tags,
569 updates: Default::default(),
570 connector_config: None,
571 encryption_config: None,
572 access_config: build_access_config(body.get("accessConfig")),
573 upgrade_policy: build_upgrade_policy(body.get("upgradePolicy")),
574 compute_config: body.get("computeConfig").cloned(),
575 storage_config: body.get("storageConfig").cloned(),
576 zonal_shift_config: body.get("zonalShiftConfig").cloned(),
577 remote_network_config: body.get("remoteNetworkConfig").cloned(),
578 control_plane_scaling_config: body.get("controlPlaneScalingConfig").cloned(),
579 deletion_protection: body.get("deletionProtection").and_then(|v| v.as_bool()),
580 };
581
582 let out = cluster_json(&cluster, &id);
583 state.clusters.insert(name, cluster);
584 Ok(AwsResponse::json(
585 StatusCode::OK,
586 json!({ "cluster": out }).to_string(),
587 ))
588 }
589
590 fn describe_cluster(
591 &self,
592 req: &AwsRequest,
593 name: &str,
594 ) -> Result<AwsResponse, AwsServiceError> {
595 let mut accounts = self.state.write();
596 let state = accounts.get_or_create(&req.account_id);
597 let cluster = state
598 .clusters
599 .get_mut(name)
600 .ok_or_else(not_found_cluster(name))?;
601 if cluster.status == "CREATING" {
604 cluster.status = "ACTIVE".to_string();
605 }
606 let id = arn_cluster_id(&cluster.endpoint);
607 Ok(AwsResponse::json(
608 StatusCode::OK,
609 json!({ "cluster": cluster_json(cluster, &id) }).to_string(),
610 ))
611 }
612
613 fn list_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
614 let max_results = validate_max_results(req)?;
615 let next_token = req.query_params.get("nextToken").cloned();
616
617 let accounts = self.state.read();
618 let Some(state) = accounts.get(&req.account_id) else {
619 return Ok(AwsResponse::json(
620 StatusCode::OK,
621 json!({ "clusters": [] }).to_string(),
622 ));
623 };
624 let names: Vec<String> = state.clusters.keys().cloned().collect();
625 let (page, token) = paginate_checked(&names, next_token.as_deref(), max_results)
626 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
627 let mut out = json!({ "clusters": page });
628 if let Some(t) = token {
629 out["nextToken"] = Value::String(t);
630 }
631 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
632 }
633
634 fn delete_cluster(&self, req: &AwsRequest, name: &str) -> Result<AwsResponse, AwsServiceError> {
635 let mut accounts = self.state.write();
636 let state = accounts.get_or_create(&req.account_id);
637 if !state.clusters.contains_key(name) {
638 return Err(not_found_cluster(name)());
639 }
640 let has_nodegroups = state.nodegroups.get(name).is_some_and(|m| !m.is_empty());
644 let has_fargate = state
645 .fargate_profiles
646 .get(name)
647 .is_some_and(|m| !m.is_empty());
648 if has_nodegroups || has_fargate {
649 return Err(AwsServiceError::aws_error(
650 StatusCode::CONFLICT,
651 "ResourceInUseException",
652 format!(
653 "Cluster has {} attached that must be deleted first",
654 if has_nodegroups {
655 "nodegroups"
656 } else {
657 "Fargate profiles"
658 }
659 ),
660 ));
661 }
662 state.nodegroups.remove(name);
665 state.fargate_profiles.remove(name);
666 state.addons.remove(name);
667 state.access_entries.remove(name);
668 state.identity_provider_configs.remove(name);
669 state.pod_identity_associations.remove(name);
670 state.insights.remove(name);
671 state.insights_refresh.remove(name);
672 state.capabilities.remove(name);
673 let mut cluster = state
674 .clusters
675 .remove(name)
676 .expect("existence checked above");
677 cluster.status = "DELETING".to_string();
678 let id = arn_cluster_id(&cluster.endpoint);
679 Ok(AwsResponse::json(
680 StatusCode::OK,
681 json!({ "cluster": cluster_json(&cluster, &id) }).to_string(),
682 ))
683 }
684
685 fn update_cluster_config(
686 &self,
687 req: &AwsRequest,
688 name: &str,
689 ) -> Result<AwsResponse, AwsServiceError> {
690 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
691 let mut accounts = self.state.write();
692 let state = accounts.get_or_create(&req.account_id);
693 let cluster = state
694 .clusters
695 .get_mut(name)
696 .ok_or_else(not_found_cluster(name))?;
697
698 let mut update_type = "ConfigUpdate";
703 let mut matched = false;
704 let mut params: Vec<(String, String)> = Vec::new();
705 macro_rules! mark {
706 ($ty:expr) => {
707 if !matched {
708 update_type = $ty;
709 matched = true;
710 }
711 };
712 }
713
714 if let Some(logging) = body.get("logging") {
715 cluster.logging = build_logging(Some(logging));
716 mark!("LoggingUpdate");
717 params.push(("ClusterLogging".to_string(), logging.to_string()));
718 }
719 if let Some(vpc) = body.get("resourcesVpcConfig") {
720 let id = arn_cluster_id(&cluster.endpoint);
721 cluster.resources_vpc_config = build_vpc_config_response(vpc, &id);
722 mark!("VpcConfigUpdate");
723 params.push(("ResourcesVpcConfig".to_string(), vpc.to_string()));
724 }
725 if let Some(ac) = body.get("accessConfig") {
726 cluster.access_config = build_access_config(Some(ac));
727 mark!("AccessConfigUpdate");
728 params.push((
729 "AuthenticationMode".to_string(),
730 cluster.access_config["authenticationMode"].to_string(),
731 ));
732 }
733 if let Some(up) = body.get("upgradePolicy") {
734 cluster.upgrade_policy = build_upgrade_policy(Some(up));
735 mark!("UpgradePolicyUpdate");
736 params.push((
737 "SupportType".to_string(),
738 cluster.upgrade_policy["supportType"].to_string(),
739 ));
740 }
741 if let Some(kn) = body.get("kubernetesNetworkConfig") {
742 cluster.kubernetes_network_config = build_k8s_network_config(Some(kn));
743 mark!("VpcConfigUpdate");
744 params.push(("KubernetesNetworkConfig".to_string(), kn.to_string()));
745 }
746 if let Some(cc) = body.get("computeConfig") {
747 cluster.compute_config = Some(cc.clone());
748 mark!("AutoModeUpdate");
749 params.push(("ComputeConfig".to_string(), cc.to_string()));
750 }
751 if let Some(sc) = body.get("storageConfig") {
752 cluster.storage_config = Some(sc.clone());
753 mark!("AutoModeUpdate");
754 params.push(("StorageConfig".to_string(), sc.to_string()));
755 }
756 if let Some(z) = body.get("zonalShiftConfig") {
757 cluster.zonal_shift_config = Some(z.clone());
758 mark!("ZonalShiftConfigUpdate");
759 params.push(("ZonalShiftConfig".to_string(), z.to_string()));
760 }
761 if let Some(rn) = body.get("remoteNetworkConfig") {
762 cluster.remote_network_config = Some(rn.clone());
763 mark!("RemoteNetworkConfigUpdate");
764 params.push(("RemoteNetworkConfig".to_string(), rn.to_string()));
765 }
766 if let Some(sp) = body.get("controlPlaneScalingConfig") {
767 cluster.control_plane_scaling_config = Some(sp.clone());
768 mark!("ControlPlaneScalingConfigUpdate");
769 params.push(("ControlPlaneScalingConfig".to_string(), sp.to_string()));
770 }
771 if let Some(dp) = body.get("deletionProtection").and_then(|v| v.as_bool()) {
772 cluster.deletion_protection = Some(dp);
773 mark!("DeletionProtectionUpdate");
774 params.push(("DeletionProtection".to_string(), dp.to_string()));
775 }
776 let _ = matched;
777
778 let update = new_update(update_type, params);
779 let out = update_json(&update);
780 cluster.updates.insert(update.id.clone(), update);
781 Ok(AwsResponse::json(
782 StatusCode::OK,
783 json!({ "update": out }).to_string(),
784 ))
785 }
786
787 fn update_cluster_version(
788 &self,
789 req: &AwsRequest,
790 name: &str,
791 ) -> Result<AwsResponse, AwsServiceError> {
792 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
793 let version = body
794 .get("version")
795 .and_then(|v| v.as_str())
796 .ok_or_else(|| invalid_parameter("version is required"))?
797 .to_string();
798
799 let mut accounts = self.state.write();
800 let state = accounts.get_or_create(&req.account_id);
801 let cluster = state
802 .clusters
803 .get_mut(name)
804 .ok_or_else(not_found_cluster(name))?;
805 cluster.version = version.clone();
806
807 let update = new_update(
808 "VersionUpdate",
809 vec![
810 ("Version".to_string(), version),
811 (
812 "PlatformVersion".to_string(),
813 cluster.platform_version.clone(),
814 ),
815 ],
816 );
817 let out = update_json(&update);
818 cluster.updates.insert(update.id.clone(), update);
819 Ok(AwsResponse::json(
820 StatusCode::OK,
821 json!({ "update": out }).to_string(),
822 ))
823 }
824
825 fn describe_update(
826 &self,
827 req: &AwsRequest,
828 name: &str,
829 update_id: &str,
830 ) -> Result<AwsResponse, AwsServiceError> {
831 let nodegroup_name = req.query_params.get("nodegroupName").cloned();
832 let addon_name = req.query_params.get("addonName").cloned();
833 let mut accounts = self.state.write();
834 let state = accounts.get_or_create(&req.account_id);
835 if !state.clusters.contains_key(name) {
836 return Err(not_found_cluster(name)());
837 }
838 let update = if let Some(ng_name) = nodegroup_name.as_deref() {
843 let ng = state
844 .nodegroups
845 .get_mut(name)
846 .and_then(|m| m.get_mut(ng_name))
847 .ok_or_else(not_found_nodegroup(ng_name))?;
848 ng.updates
849 .get_mut(update_id)
850 .ok_or_else(not_found_update(update_id))?
851 } else if let Some(a_name) = addon_name.as_deref() {
852 let addon = state
853 .addons
854 .get_mut(name)
855 .and_then(|m| m.get_mut(a_name))
856 .ok_or_else(not_found_addon(a_name))?;
857 addon
858 .updates
859 .get_mut(update_id)
860 .ok_or_else(not_found_update(update_id))?
861 } else {
862 let cluster = state
863 .clusters
864 .get_mut(name)
865 .ok_or_else(not_found_cluster(name))?;
866 cluster
867 .updates
868 .get_mut(update_id)
869 .ok_or_else(not_found_update(update_id))?
870 };
871 if update.status == "InProgress" {
873 update.status = "Successful".to_string();
874 }
875 Ok(AwsResponse::json(
876 StatusCode::OK,
877 json!({ "update": update_json(update) }).to_string(),
878 ))
879 }
880
881 fn list_updates(&self, req: &AwsRequest, name: &str) -> Result<AwsResponse, AwsServiceError> {
882 let max_results = validate_max_results(req)?;
883 let next_token = req.query_params.get("nextToken").cloned();
884
885 let nodegroup_name = req.query_params.get("nodegroupName").cloned();
886 let addon_name = req.query_params.get("addonName").cloned();
887 let accounts = self.state.read();
888 let state = accounts
889 .get(&req.account_id)
890 .ok_or_else(not_found_cluster(name))?;
891 if !state.clusters.contains_key(name) {
892 return Err(not_found_cluster(name)());
893 }
894 let ids: Vec<String> = if let Some(ng_name) = nodegroup_name.as_deref() {
895 let ng = state
896 .nodegroups
897 .get(name)
898 .and_then(|m| m.get(ng_name))
899 .ok_or_else(not_found_nodegroup(ng_name))?;
900 ng.updates.keys().cloned().collect()
901 } else if let Some(a_name) = addon_name.as_deref() {
902 let addon = state
903 .addons
904 .get(name)
905 .and_then(|m| m.get(a_name))
906 .ok_or_else(not_found_addon(a_name))?;
907 addon.updates.keys().cloned().collect()
908 } else {
909 let cluster = state
910 .clusters
911 .get(name)
912 .ok_or_else(not_found_cluster(name))?;
913 cluster.updates.keys().cloned().collect()
914 };
915 let (page, token) = paginate_checked(&ids, next_token.as_deref(), max_results)
916 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
917 let mut out = json!({ "updateIds": page });
918 if let Some(t) = token {
919 out["nextToken"] = Value::String(t);
920 }
921 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
922 }
923
924 fn tag_resource(&self, req: &AwsRequest, arn: &str) -> Result<AwsResponse, AwsServiceError> {
925 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
926 let tags = body
927 .get("tags")
928 .filter(|v| v.is_object())
929 .ok_or_else(|| bad_request("tags is required"))?;
930 validate_eks_arn(arn)?;
931 let mut accounts = self.state.write();
932 let state = accounts.get_or_create(&req.account_id);
933 let target = locate_tag_target(state, arn);
937 let dest = tags_mut(state, &target);
938 for (k, v) in tags.as_object().unwrap() {
939 if let Some(v) = v.as_str() {
940 dest.insert(k.clone(), v.to_string());
941 }
942 }
943 Ok(AwsResponse::json(StatusCode::OK, "{}"))
944 }
945
946 fn untag_resource(&self, req: &AwsRequest, arn: &str) -> Result<AwsResponse, AwsServiceError> {
947 let keys = parse_multi_query(&req.raw_query, "tagKeys");
948 validate_eks_arn(arn)?;
949 let mut accounts = self.state.write();
950 let state = accounts.get_or_create(&req.account_id);
951 let target = locate_tag_target(state, arn);
952 let dest = tags_mut(state, &target);
953 for k in keys {
954 dest.remove(&k);
955 }
956 Ok(AwsResponse::json(StatusCode::OK, "{}"))
957 }
958
959 fn list_tags_for_resource(
960 &self,
961 req: &AwsRequest,
962 arn: &str,
963 ) -> Result<AwsResponse, AwsServiceError> {
964 validate_eks_arn(arn)?;
965 let accounts = self.state.read();
966 let tags: serde_json::Map<String, Value> = accounts
967 .get(&req.account_id)
968 .map(|state| {
969 let target = locate_tag_target(state, arn);
970 tags_ref(state, &target)
971 .map(|t| {
972 t.iter()
973 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
974 .collect()
975 })
976 .unwrap_or_default()
977 })
978 .unwrap_or_default();
979 Ok(AwsResponse::json(
980 StatusCode::OK,
981 json!({ "tags": tags }).to_string(),
982 ))
983 }
984
985 fn create_nodegroup(
990 &self,
991 req: &AwsRequest,
992 cluster_name: &str,
993 ) -> Result<AwsResponse, AwsServiceError> {
994 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
995 let name = body
996 .get("nodegroupName")
997 .and_then(|v| v.as_str())
998 .ok_or_else(|| invalid_parameter("nodegroupName is required"))?
999 .to_string();
1000 let node_role = body
1001 .get("nodeRole")
1002 .and_then(|v| v.as_str())
1003 .ok_or_else(|| invalid_parameter("nodeRole is required"))?
1004 .to_string();
1005 let subnets = body
1006 .get("subnets")
1007 .filter(|v| v.is_array())
1008 .cloned()
1009 .ok_or_else(|| invalid_parameter("subnets is required"))?;
1010
1011 let region = req.region.clone();
1012 let account_id = req.account_id.clone();
1013
1014 let mut accounts = self.state.write();
1015 let state = accounts.get_or_create(&req.account_id);
1016 let cluster_version = state
1020 .clusters
1021 .get(cluster_name)
1022 .ok_or_else(not_found_cluster(cluster_name))?
1023 .version
1024 .clone();
1025 if state
1026 .nodegroups
1027 .get(cluster_name)
1028 .is_some_and(|m| m.contains_key(&name))
1029 {
1030 return Err(AwsServiceError::aws_error(
1031 StatusCode::CONFLICT,
1032 "ResourceInUseException",
1033 format!(
1034 "NodeGroup already exists with name {name} and cluster name {cluster_name}"
1035 ),
1036 ));
1037 }
1038
1039 let id = uuid::Uuid::new_v4().to_string();
1040 let arn = nodegroup_arn(®ion, &account_id, cluster_name, &name, &id);
1041 let now = Utc::now();
1042 let version = body
1043 .get("version")
1044 .and_then(|v| v.as_str())
1045 .map(|s| s.to_string())
1046 .unwrap_or(cluster_version);
1047 let release_version = body
1048 .get("releaseVersion")
1049 .and_then(|v| v.as_str())
1050 .map(|s| s.to_string())
1051 .unwrap_or_else(|| format!("{version}-20240000"));
1052 let capacity_type = body
1053 .get("capacityType")
1054 .and_then(|v| v.as_str())
1055 .unwrap_or("ON_DEMAND")
1056 .to_string();
1057 let ami_type = body
1058 .get("amiType")
1059 .and_then(|v| v.as_str())
1060 .unwrap_or("AL2023_x86_64_STANDARD")
1061 .to_string();
1062 let disk_size = body.get("diskSize").and_then(|v| v.as_i64()).unwrap_or(20);
1063
1064 let ng = Nodegroup {
1065 name: name.clone(),
1066 arn,
1067 cluster_name: cluster_name.to_string(),
1068 version,
1069 release_version,
1070 status: "CREATING".to_string(),
1071 capacity_type,
1072 ami_type,
1073 node_role,
1074 created_at: now,
1075 modified_at: now,
1076 disk_size,
1077 scaling_config: build_scaling_config(body.get("scalingConfig")),
1078 update_config: build_nodegroup_update_config(body.get("updateConfig")),
1079 instance_types: body
1080 .get("instanceTypes")
1081 .cloned()
1082 .unwrap_or_else(|| json!(["t3.medium"])),
1083 subnets,
1084 labels: body.get("labels").cloned().unwrap_or_else(|| json!({})),
1085 taints: body.get("taints").cloned().unwrap_or_else(|| json!([])),
1086 remote_access: body.get("remoteAccess").cloned(),
1087 launch_template: body.get("launchTemplate").cloned(),
1088 asg_name: format!("eks-{name}-{}", &id[..8]),
1089 tags: parse_tag_map(body.get("tags")),
1090 updates: Default::default(),
1091 };
1092
1093 let out = nodegroup_json(&ng);
1094 state
1095 .nodegroups
1096 .entry(cluster_name.to_string())
1097 .or_default()
1098 .insert(name, ng);
1099 Ok(AwsResponse::json(
1100 StatusCode::OK,
1101 json!({ "nodegroup": out }).to_string(),
1102 ))
1103 }
1104
1105 fn describe_nodegroup(
1106 &self,
1107 req: &AwsRequest,
1108 cluster_name: &str,
1109 name: &str,
1110 ) -> Result<AwsResponse, AwsServiceError> {
1111 let mut accounts = self.state.write();
1112 let state = accounts.get_or_create(&req.account_id);
1113 if !state.clusters.contains_key(cluster_name) {
1114 return Err(not_found_cluster(cluster_name)());
1115 }
1116 let ng = state
1117 .nodegroups
1118 .get_mut(cluster_name)
1119 .and_then(|m| m.get_mut(name))
1120 .ok_or_else(not_found_nodegroup(name))?;
1121 if ng.status == "CREATING" {
1122 ng.status = "ACTIVE".to_string();
1123 }
1124 Ok(AwsResponse::json(
1125 StatusCode::OK,
1126 json!({ "nodegroup": nodegroup_json(ng) }).to_string(),
1127 ))
1128 }
1129
1130 fn list_nodegroups(
1131 &self,
1132 req: &AwsRequest,
1133 cluster_name: &str,
1134 ) -> Result<AwsResponse, AwsServiceError> {
1135 let max_results = validate_max_results(req)?;
1136 let next_token = req.query_params.get("nextToken").cloned();
1137 let accounts = self.state.read();
1138 let state = accounts
1139 .get(&req.account_id)
1140 .ok_or_else(not_found_cluster(cluster_name))?;
1141 if !state.clusters.contains_key(cluster_name) {
1142 return Err(not_found_cluster(cluster_name)());
1143 }
1144 let names: Vec<String> = state
1145 .nodegroups
1146 .get(cluster_name)
1147 .map(|m| m.keys().cloned().collect())
1148 .unwrap_or_default();
1149 let (page, token) = paginate_checked(&names, next_token.as_deref(), max_results)
1150 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
1151 let mut out = json!({ "nodegroups": page });
1152 if let Some(t) = token {
1153 out["nextToken"] = Value::String(t);
1154 }
1155 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
1156 }
1157
1158 fn delete_nodegroup(
1159 &self,
1160 req: &AwsRequest,
1161 cluster_name: &str,
1162 name: &str,
1163 ) -> Result<AwsResponse, AwsServiceError> {
1164 let mut accounts = self.state.write();
1165 let state = accounts.get_or_create(&req.account_id);
1166 if !state.clusters.contains_key(cluster_name) {
1167 return Err(not_found_cluster(cluster_name)());
1168 }
1169 let mut ng = state
1170 .nodegroups
1171 .get_mut(cluster_name)
1172 .and_then(|m| m.remove(name))
1173 .ok_or_else(not_found_nodegroup(name))?;
1174 ng.status = "DELETING".to_string();
1175 Ok(AwsResponse::json(
1176 StatusCode::OK,
1177 json!({ "nodegroup": nodegroup_json(&ng) }).to_string(),
1178 ))
1179 }
1180
1181 fn update_nodegroup_config(
1182 &self,
1183 req: &AwsRequest,
1184 cluster_name: &str,
1185 name: &str,
1186 ) -> Result<AwsResponse, AwsServiceError> {
1187 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1188 let mut accounts = self.state.write();
1189 let state = accounts.get_or_create(&req.account_id);
1190 if !state.clusters.contains_key(cluster_name) {
1191 return Err(not_found_cluster(cluster_name)());
1192 }
1193 let ng = state
1194 .nodegroups
1195 .get_mut(cluster_name)
1196 .and_then(|m| m.get_mut(name))
1197 .ok_or_else(not_found_nodegroup(name))?;
1198
1199 let mut params = Vec::new();
1200 if let Some(scaling) = body.get("scalingConfig") {
1201 ng.scaling_config = build_scaling_config(Some(scaling));
1202 params.push(("ScalingConfig".to_string(), scaling.to_string()));
1203 }
1204 if let Some(labels) = body.get("labels") {
1205 if let Some(add) = labels.get("addOrUpdateLabels").and_then(|v| v.as_object()) {
1206 let map = ng.labels.as_object_mut();
1207 if let Some(map) = map {
1208 for (k, v) in add {
1209 map.insert(k.clone(), v.clone());
1210 }
1211 }
1212 }
1213 params.push(("LabelsToAdd".to_string(), labels.to_string()));
1214 }
1215 if let Some(update_config) = body.get("updateConfig") {
1216 ng.update_config = build_nodegroup_update_config(Some(update_config));
1217 params.push(("MaxUnavailable".to_string(), update_config.to_string()));
1218 }
1219 ng.modified_at = Utc::now();
1220
1221 let update = new_update("ConfigUpdate", params);
1222 let out = update_json(&update);
1223 ng.updates.insert(update.id.clone(), update);
1224 Ok(AwsResponse::json(
1225 StatusCode::OK,
1226 json!({ "update": out }).to_string(),
1227 ))
1228 }
1229
1230 fn update_nodegroup_version(
1231 &self,
1232 req: &AwsRequest,
1233 cluster_name: &str,
1234 name: &str,
1235 ) -> Result<AwsResponse, AwsServiceError> {
1236 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1237 let mut accounts = self.state.write();
1238 let state = accounts.get_or_create(&req.account_id);
1239 if !state.clusters.contains_key(cluster_name) {
1240 return Err(not_found_cluster(cluster_name)());
1241 }
1242 let ng = state
1243 .nodegroups
1244 .get_mut(cluster_name)
1245 .and_then(|m| m.get_mut(name))
1246 .ok_or_else(not_found_nodegroup(name))?;
1247
1248 let mut params = Vec::new();
1249 if let Some(version) = body.get("version").and_then(|v| v.as_str()) {
1250 ng.version = version.to_string();
1251 params.push(("Version".to_string(), version.to_string()));
1252 }
1253 if let Some(release) = body.get("releaseVersion").and_then(|v| v.as_str()) {
1254 ng.release_version = release.to_string();
1255 params.push(("ReleaseVersion".to_string(), release.to_string()));
1256 }
1257 ng.modified_at = Utc::now();
1258
1259 let update = new_update("VersionUpdate", params);
1260 let out = update_json(&update);
1261 ng.updates.insert(update.id.clone(), update);
1262 Ok(AwsResponse::json(
1263 StatusCode::OK,
1264 json!({ "update": out }).to_string(),
1265 ))
1266 }
1267
1268 fn create_fargate_profile(
1273 &self,
1274 req: &AwsRequest,
1275 cluster_name: &str,
1276 ) -> Result<AwsResponse, AwsServiceError> {
1277 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1278 let name = body
1279 .get("fargateProfileName")
1280 .and_then(|v| v.as_str())
1281 .ok_or_else(|| invalid_parameter("fargateProfileName is required"))?
1282 .to_string();
1283 let pod_execution_role_arn = body
1284 .get("podExecutionRoleArn")
1285 .and_then(|v| v.as_str())
1286 .ok_or_else(|| invalid_parameter("podExecutionRoleArn is required"))?
1287 .to_string();
1288
1289 let region = req.region.clone();
1290 let account_id = req.account_id.clone();
1291
1292 let mut accounts = self.state.write();
1293 let state = accounts.get_or_create(&req.account_id);
1294 if !state.clusters.contains_key(cluster_name) {
1295 return Err(not_found_cluster(cluster_name)());
1296 }
1297 if state
1298 .fargate_profiles
1299 .get(cluster_name)
1300 .is_some_and(|m| m.contains_key(&name))
1301 {
1302 return Err(AwsServiceError::aws_error(
1303 StatusCode::CONFLICT,
1304 "ResourceInUseException",
1305 format!(
1306 "FargateProfile already exists with name {name} and cluster name {cluster_name}"
1307 ),
1308 ));
1309 }
1310
1311 let id = uuid::Uuid::new_v4().to_string();
1312 let arn = fargate_profile_arn(®ion, &account_id, cluster_name, &name, &id);
1313 let profile = FargateProfile {
1314 name: name.clone(),
1315 arn,
1316 cluster_name: cluster_name.to_string(),
1317 pod_execution_role_arn,
1318 status: "CREATING".to_string(),
1319 created_at: Utc::now(),
1320 subnets: body.get("subnets").cloned().unwrap_or_else(|| json!([])),
1321 selectors: body.get("selectors").cloned().unwrap_or_else(|| json!([])),
1322 tags: parse_tag_map(body.get("tags")),
1323 };
1324
1325 let out = fargate_profile_json(&profile);
1326 state
1327 .fargate_profiles
1328 .entry(cluster_name.to_string())
1329 .or_default()
1330 .insert(name, profile);
1331 Ok(AwsResponse::json(
1332 StatusCode::OK,
1333 json!({ "fargateProfile": out }).to_string(),
1334 ))
1335 }
1336
1337 fn describe_fargate_profile(
1338 &self,
1339 req: &AwsRequest,
1340 cluster_name: &str,
1341 name: &str,
1342 ) -> Result<AwsResponse, AwsServiceError> {
1343 let mut accounts = self.state.write();
1344 let state = accounts.get_or_create(&req.account_id);
1345 if !state.clusters.contains_key(cluster_name) {
1346 return Err(not_found_cluster(cluster_name)());
1347 }
1348 let profile = state
1349 .fargate_profiles
1350 .get_mut(cluster_name)
1351 .and_then(|m| m.get_mut(name))
1352 .ok_or_else(not_found_fargate_profile(name))?;
1353 if profile.status == "CREATING" {
1354 profile.status = "ACTIVE".to_string();
1355 }
1356 Ok(AwsResponse::json(
1357 StatusCode::OK,
1358 json!({ "fargateProfile": fargate_profile_json(profile) }).to_string(),
1359 ))
1360 }
1361
1362 fn list_fargate_profiles(
1363 &self,
1364 req: &AwsRequest,
1365 cluster_name: &str,
1366 ) -> Result<AwsResponse, AwsServiceError> {
1367 let max_results = validate_max_results(req)?;
1368 let next_token = req.query_params.get("nextToken").cloned();
1369 let accounts = self.state.read();
1370 let state = accounts
1371 .get(&req.account_id)
1372 .ok_or_else(not_found_cluster(cluster_name))?;
1373 if !state.clusters.contains_key(cluster_name) {
1374 return Err(not_found_cluster(cluster_name)());
1375 }
1376 let names: Vec<String> = state
1377 .fargate_profiles
1378 .get(cluster_name)
1379 .map(|m| m.keys().cloned().collect())
1380 .unwrap_or_default();
1381 let (page, token) = paginate_checked(&names, next_token.as_deref(), max_results)
1382 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
1383 let mut out = json!({ "fargateProfileNames": page });
1384 if let Some(t) = token {
1385 out["nextToken"] = Value::String(t);
1386 }
1387 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
1388 }
1389
1390 fn delete_fargate_profile(
1391 &self,
1392 req: &AwsRequest,
1393 cluster_name: &str,
1394 name: &str,
1395 ) -> Result<AwsResponse, AwsServiceError> {
1396 let mut accounts = self.state.write();
1397 let state = accounts.get_or_create(&req.account_id);
1398 if !state.clusters.contains_key(cluster_name) {
1399 return Err(not_found_cluster(cluster_name)());
1400 }
1401 let mut profile = state
1402 .fargate_profiles
1403 .get_mut(cluster_name)
1404 .and_then(|m| m.remove(name))
1405 .ok_or_else(not_found_fargate_profile(name))?;
1406 profile.status = "DELETING".to_string();
1407 Ok(AwsResponse::json(
1408 StatusCode::OK,
1409 json!({ "fargateProfile": fargate_profile_json(&profile) }).to_string(),
1410 ))
1411 }
1412
1413 fn create_addon(
1418 &self,
1419 req: &AwsRequest,
1420 cluster_name: &str,
1421 ) -> Result<AwsResponse, AwsServiceError> {
1422 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1423 let name = body
1424 .get("addonName")
1425 .and_then(|v| v.as_str())
1426 .ok_or_else(|| invalid_parameter("addonName is required"))?
1427 .to_string();
1428
1429 let region = req.region.clone();
1430 let account_id = req.account_id.clone();
1431
1432 let mut accounts = self.state.write();
1433 let state = accounts.get_or_create(&req.account_id);
1434 let cluster_version = state
1436 .clusters
1437 .get(cluster_name)
1438 .ok_or_else(not_found_cluster(cluster_name))?
1439 .version
1440 .clone();
1441 if state
1442 .addons
1443 .get(cluster_name)
1444 .is_some_and(|m| m.contains_key(&name))
1445 {
1446 return Err(AwsServiceError::aws_error(
1447 StatusCode::CONFLICT,
1448 "ResourceInUseException",
1449 format!("Addon already exists with name {name} and cluster name {cluster_name}"),
1450 ));
1451 }
1452
1453 let id = uuid::Uuid::new_v4().to_string();
1454 let arn = addon_arn(®ion, &account_id, cluster_name, &name, &id);
1455 let now = Utc::now();
1456 let addon_version = body
1457 .get("addonVersion")
1458 .and_then(|v| v.as_str())
1459 .map(|s| s.to_string())
1460 .unwrap_or_else(|| default_addon_version(&name, &cluster_version));
1461 let namespace = body
1462 .get("namespaceConfig")
1463 .and_then(|v| v.get("namespace"))
1464 .and_then(|v| v.as_str())
1465 .map(|s| s.to_string());
1466 let pod_identity_associations = build_pod_identity_association_arns(
1467 ®ion,
1468 &account_id,
1469 cluster_name,
1470 body.get("podIdentityAssociations"),
1471 );
1472
1473 let addon = Addon {
1474 name: name.clone(),
1475 arn,
1476 cluster_name: cluster_name.to_string(),
1477 addon_version,
1478 status: "CREATING".to_string(),
1479 created_at: now,
1480 modified_at: now,
1481 service_account_role_arn: body
1482 .get("serviceAccountRoleArn")
1483 .and_then(|v| v.as_str())
1484 .map(|s| s.to_string()),
1485 configuration_values: body
1486 .get("configurationValues")
1487 .and_then(|v| v.as_str())
1488 .map(|s| s.to_string()),
1489 namespace,
1490 pod_identity_associations,
1491 tags: parse_tag_map(body.get("tags")),
1492 updates: Default::default(),
1493 };
1494
1495 let out = addon_json(&addon);
1496 state
1497 .addons
1498 .entry(cluster_name.to_string())
1499 .or_default()
1500 .insert(name, addon);
1501 Ok(AwsResponse::json(
1502 StatusCode::OK,
1503 json!({ "addon": out }).to_string(),
1504 ))
1505 }
1506
1507 fn describe_addon(
1508 &self,
1509 req: &AwsRequest,
1510 cluster_name: &str,
1511 name: &str,
1512 ) -> Result<AwsResponse, AwsServiceError> {
1513 let mut accounts = self.state.write();
1514 let state = accounts.get_or_create(&req.account_id);
1515 if !state.clusters.contains_key(cluster_name) {
1516 return Err(not_found_cluster(cluster_name)());
1517 }
1518 let addon = state
1519 .addons
1520 .get_mut(cluster_name)
1521 .and_then(|m| m.get_mut(name))
1522 .ok_or_else(not_found_addon(name))?;
1523 if addon.status == "CREATING" {
1525 addon.status = "ACTIVE".to_string();
1526 }
1527 Ok(AwsResponse::json(
1528 StatusCode::OK,
1529 json!({ "addon": addon_json(addon) }).to_string(),
1530 ))
1531 }
1532
1533 fn list_addons(
1534 &self,
1535 req: &AwsRequest,
1536 cluster_name: &str,
1537 ) -> Result<AwsResponse, AwsServiceError> {
1538 let max_results = validate_max_results(req)?;
1539 let next_token = req.query_params.get("nextToken").cloned();
1540 let accounts = self.state.read();
1541 let state = accounts
1542 .get(&req.account_id)
1543 .ok_or_else(not_found_cluster(cluster_name))?;
1544 if !state.clusters.contains_key(cluster_name) {
1545 return Err(not_found_cluster(cluster_name)());
1546 }
1547 let names: Vec<String> = state
1548 .addons
1549 .get(cluster_name)
1550 .map(|m| m.keys().cloned().collect())
1551 .unwrap_or_default();
1552 let (page, token) = paginate_checked(&names, next_token.as_deref(), max_results)
1553 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
1554 let mut out = json!({ "addons": page });
1555 if let Some(t) = token {
1556 out["nextToken"] = Value::String(t);
1557 }
1558 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
1559 }
1560
1561 fn delete_addon(
1562 &self,
1563 req: &AwsRequest,
1564 cluster_name: &str,
1565 name: &str,
1566 ) -> Result<AwsResponse, AwsServiceError> {
1567 let mut accounts = self.state.write();
1568 let state = accounts.get_or_create(&req.account_id);
1569 if !state.clusters.contains_key(cluster_name) {
1570 return Err(not_found_cluster(cluster_name)());
1571 }
1572 let mut addon = state
1573 .addons
1574 .get_mut(cluster_name)
1575 .and_then(|m| m.remove(name))
1576 .ok_or_else(not_found_addon(name))?;
1577 addon.status = "DELETING".to_string();
1578 Ok(AwsResponse::json(
1579 StatusCode::OK,
1580 json!({ "addon": addon_json(&addon) }).to_string(),
1581 ))
1582 }
1583
1584 fn update_addon(
1585 &self,
1586 req: &AwsRequest,
1587 cluster_name: &str,
1588 name: &str,
1589 ) -> Result<AwsResponse, AwsServiceError> {
1590 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1591 let region = req.region.clone();
1592 let account_id = req.account_id.clone();
1593 let mut accounts = self.state.write();
1594 let state = accounts.get_or_create(&req.account_id);
1595 if !state.clusters.contains_key(cluster_name) {
1596 return Err(not_found_cluster(cluster_name)());
1597 }
1598 let addon = state
1599 .addons
1600 .get_mut(cluster_name)
1601 .and_then(|m| m.get_mut(name))
1602 .ok_or_else(not_found_addon(name))?;
1603
1604 let mut params = Vec::new();
1605 if let Some(version) = body.get("addonVersion").and_then(|v| v.as_str()) {
1606 addon.addon_version = version.to_string();
1607 params.push(("AddonVersion".to_string(), version.to_string()));
1608 }
1609 if let Some(role) = body.get("serviceAccountRoleArn").and_then(|v| v.as_str()) {
1610 addon.service_account_role_arn = Some(role.to_string());
1611 params.push(("ServiceAccountRoleArn".to_string(), role.to_string()));
1612 }
1613 if let Some(cfg) = body.get("configurationValues").and_then(|v| v.as_str()) {
1614 addon.configuration_values = Some(cfg.to_string());
1615 params.push(("ConfigurationValues".to_string(), cfg.to_string()));
1616 }
1617 if let Some(resolve) = body.get("resolveConflicts").and_then(|v| v.as_str()) {
1618 params.push(("ResolveConflicts".to_string(), resolve.to_string()));
1619 }
1620 if let Some(assocs) = body.get("podIdentityAssociations") {
1621 addon.pod_identity_associations = build_pod_identity_association_arns(
1622 ®ion,
1623 &account_id,
1624 cluster_name,
1625 Some(assocs),
1626 );
1627 }
1628 addon.modified_at = Utc::now();
1629
1630 let update = new_update("AddonUpdate", params);
1631 let out = update_json(&update);
1632 addon.updates.insert(update.id.clone(), update);
1633 Ok(AwsResponse::json(
1634 StatusCode::OK,
1635 json!({ "update": out }).to_string(),
1636 ))
1637 }
1638
1639 fn describe_addon_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1640 let max_results = validate_max_results(req)?;
1641 let next_token = req.query_params.get("nextToken").cloned();
1642 let addon_filter = req.query_params.get("addonName").cloned();
1643 let k8s_version = req
1644 .query_params
1645 .get("kubernetesVersion")
1646 .cloned()
1647 .unwrap_or_else(|| DEFAULT_K8S_VERSION.to_string());
1648
1649 let catalog = addon_catalog(&k8s_version);
1650 let filtered: Vec<Value> = catalog
1651 .into_iter()
1652 .filter(|a| addon_filter.as_deref().is_none_or(|f| a["addonName"] == f))
1653 .collect();
1654
1655 let (page, token) = paginate_checked(&filtered, next_token.as_deref(), max_results)
1656 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
1657 let mut out = json!({ "addons": page });
1658 if let Some(t) = token {
1659 out["nextToken"] = Value::String(t);
1660 }
1661 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
1662 }
1663
1664 fn describe_addon_configuration(
1665 &self,
1666 req: &AwsRequest,
1667 ) -> Result<AwsResponse, AwsServiceError> {
1668 let addon_name = req
1669 .query_params
1670 .get("addonName")
1671 .cloned()
1672 .ok_or_else(|| invalid_parameter("addonName is required"))?;
1673 let addon_version = req
1674 .query_params
1675 .get("addonVersion")
1676 .cloned()
1677 .ok_or_else(|| invalid_parameter("addonVersion is required"))?;
1678
1679 Ok(AwsResponse::json(
1680 StatusCode::OK,
1681 json!({
1682 "addonName": addon_name,
1683 "addonVersion": addon_version,
1684 "configurationSchema": addon_configuration_schema(&addon_name),
1685 "podIdentityConfiguration": pod_identity_configuration(&addon_name),
1686 })
1687 .to_string(),
1688 ))
1689 }
1690
1691 fn create_access_entry(
1696 &self,
1697 req: &AwsRequest,
1698 cluster_name: &str,
1699 ) -> Result<AwsResponse, AwsServiceError> {
1700 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1701 let principal_arn = body
1702 .get("principalArn")
1703 .and_then(|v| v.as_str())
1704 .ok_or_else(|| invalid_parameter("principalArn is required"))?
1705 .to_string();
1706
1707 let region = req.region.clone();
1708 let account_id = req.account_id.clone();
1709
1710 let mut accounts = self.state.write();
1711 let state = accounts.get_or_create(&req.account_id);
1712 if !state.clusters.contains_key(cluster_name) {
1714 return Err(not_found_cluster(cluster_name)());
1715 }
1716 if state
1717 .access_entries
1718 .get(cluster_name)
1719 .is_some_and(|m| m.contains_key(&principal_arn))
1720 {
1721 return Err(AwsServiceError::aws_error(
1722 StatusCode::CONFLICT,
1723 "ResourceInUseException",
1724 format!(
1725 "The specified access entry resource is already in use on this cluster: {principal_arn}"
1726 ),
1727 ));
1728 }
1729
1730 let (principal_type, principal_name) = principal_parts(&principal_arn);
1731 let id = uuid::Uuid::new_v4().to_string();
1732 let arn = access_entry_arn(
1733 ®ion,
1734 &account_id,
1735 cluster_name,
1736 &principal_type,
1737 &principal_name,
1738 &id,
1739 );
1740 let now = Utc::now();
1741 let username = body
1742 .get("username")
1743 .and_then(|v| v.as_str())
1744 .map(|s| s.to_string())
1745 .unwrap_or_else(|| default_username(&principal_arn));
1746 let entry_type = body
1747 .get("type")
1748 .and_then(|v| v.as_str())
1749 .unwrap_or("STANDARD")
1750 .to_string();
1751 let kubernetes_groups = string_list(body.get("kubernetesGroups"));
1752
1753 let entry = AccessEntry {
1754 principal_arn: principal_arn.clone(),
1755 cluster_name: cluster_name.to_string(),
1756 arn,
1757 kubernetes_groups,
1758 username,
1759 type_: entry_type,
1760 created_at: now,
1761 modified_at: now,
1762 tags: parse_tag_map(body.get("tags")),
1763 associated_policies: Vec::new(),
1764 };
1765
1766 let out = access_entry_json(&entry);
1767 state
1768 .access_entries
1769 .entry(cluster_name.to_string())
1770 .or_default()
1771 .insert(principal_arn, entry);
1772 Ok(AwsResponse::json(
1773 StatusCode::OK,
1774 json!({ "accessEntry": out }).to_string(),
1775 ))
1776 }
1777
1778 fn describe_access_entry(
1779 &self,
1780 req: &AwsRequest,
1781 cluster_name: &str,
1782 principal_arn: &str,
1783 ) -> Result<AwsResponse, AwsServiceError> {
1784 let accounts = self.state.read();
1785 let state = accounts
1786 .get(&req.account_id)
1787 .ok_or_else(not_found_cluster(cluster_name))?;
1788 if !state.clusters.contains_key(cluster_name) {
1789 return Err(not_found_cluster(cluster_name)());
1790 }
1791 let entry = state
1792 .access_entries
1793 .get(cluster_name)
1794 .and_then(|m| m.get(principal_arn))
1795 .ok_or_else(not_found_access_entry(principal_arn))?;
1796 Ok(AwsResponse::json(
1797 StatusCode::OK,
1798 json!({ "accessEntry": access_entry_json(entry) }).to_string(),
1799 ))
1800 }
1801
1802 fn list_access_entries(
1803 &self,
1804 req: &AwsRequest,
1805 cluster_name: &str,
1806 ) -> Result<AwsResponse, AwsServiceError> {
1807 let max_results = validate_max_results(req)?;
1808 let next_token = req.query_params.get("nextToken").cloned();
1809 let associated_policy = req.query_params.get("associatedPolicyArn").cloned();
1810 let accounts = self.state.read();
1811 let state = accounts
1812 .get(&req.account_id)
1813 .ok_or_else(not_found_cluster(cluster_name))?;
1814 if !state.clusters.contains_key(cluster_name) {
1815 return Err(not_found_cluster(cluster_name)());
1816 }
1817 let names: Vec<String> = state
1820 .access_entries
1821 .get(cluster_name)
1822 .map(|m| {
1823 m.values()
1824 .filter(|e| {
1825 associated_policy.as_deref().is_none_or(|p| {
1826 e.associated_policies.iter().any(|ap| ap.policy_arn == p)
1827 })
1828 })
1829 .map(|e| e.principal_arn.clone())
1830 .collect()
1831 })
1832 .unwrap_or_default();
1833 let (page, token) = paginate_checked(&names, next_token.as_deref(), max_results)
1834 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
1835 let mut out = json!({ "accessEntries": page });
1836 if let Some(t) = token {
1837 out["nextToken"] = Value::String(t);
1838 }
1839 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
1840 }
1841
1842 fn delete_access_entry(
1843 &self,
1844 req: &AwsRequest,
1845 cluster_name: &str,
1846 principal_arn: &str,
1847 ) -> Result<AwsResponse, AwsServiceError> {
1848 let mut accounts = self.state.write();
1849 let state = accounts.get_or_create(&req.account_id);
1850 if !state.clusters.contains_key(cluster_name) {
1851 return Err(not_found_cluster(cluster_name)());
1852 }
1853 state
1854 .access_entries
1855 .get_mut(cluster_name)
1856 .and_then(|m| m.remove(principal_arn))
1857 .ok_or_else(not_found_access_entry(principal_arn))?;
1858 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1859 }
1860
1861 fn update_access_entry(
1862 &self,
1863 req: &AwsRequest,
1864 cluster_name: &str,
1865 principal_arn: &str,
1866 ) -> Result<AwsResponse, AwsServiceError> {
1867 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1868 let mut accounts = self.state.write();
1869 let state = accounts.get_or_create(&req.account_id);
1870 if !state.clusters.contains_key(cluster_name) {
1871 return Err(not_found_cluster(cluster_name)());
1872 }
1873 let entry = state
1874 .access_entries
1875 .get_mut(cluster_name)
1876 .and_then(|m| m.get_mut(principal_arn))
1877 .ok_or_else(not_found_access_entry(principal_arn))?;
1878
1879 if let Some(groups) = body.get("kubernetesGroups") {
1880 entry.kubernetes_groups = string_list(Some(groups));
1881 }
1882 if let Some(username) = body.get("username").and_then(|v| v.as_str()) {
1883 entry.username = username.to_string();
1884 }
1885 entry.modified_at = Utc::now();
1886 Ok(AwsResponse::json(
1887 StatusCode::OK,
1888 json!({ "accessEntry": access_entry_json(entry) }).to_string(),
1889 ))
1890 }
1891
1892 fn associate_access_policy(
1893 &self,
1894 req: &AwsRequest,
1895 cluster_name: &str,
1896 principal_arn: &str,
1897 ) -> Result<AwsResponse, AwsServiceError> {
1898 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
1899 let policy_arn = body
1900 .get("policyArn")
1901 .and_then(|v| v.as_str())
1902 .ok_or_else(|| invalid_parameter("policyArn is required"))?
1903 .to_string();
1904 let access_scope = build_access_scope(body.get("accessScope"));
1905
1906 let mut accounts = self.state.write();
1907 let state = accounts.get_or_create(&req.account_id);
1908 if !state.clusters.contains_key(cluster_name) {
1909 return Err(not_found_cluster(cluster_name)());
1910 }
1911 let entry = state
1912 .access_entries
1913 .get_mut(cluster_name)
1914 .and_then(|m| m.get_mut(principal_arn))
1915 .ok_or_else(not_found_access_entry(principal_arn))?;
1916
1917 let now = Utc::now();
1918 let associated = if let Some(existing) = entry
1921 .associated_policies
1922 .iter_mut()
1923 .find(|ap| ap.policy_arn == policy_arn)
1924 {
1925 existing.access_scope = access_scope;
1926 existing.modified_at = now;
1927 existing.clone()
1928 } else {
1929 let ap = AssociatedPolicy {
1930 policy_arn: policy_arn.clone(),
1931 access_scope,
1932 associated_at: now,
1933 modified_at: now,
1934 };
1935 entry.associated_policies.push(ap.clone());
1936 ap
1937 };
1938 Ok(AwsResponse::json(
1939 StatusCode::OK,
1940 json!({
1941 "clusterName": cluster_name,
1942 "principalArn": entry.principal_arn,
1943 "associatedAccessPolicy": associated_policy_json(&associated),
1944 })
1945 .to_string(),
1946 ))
1947 }
1948
1949 fn disassociate_access_policy(
1950 &self,
1951 req: &AwsRequest,
1952 cluster_name: &str,
1953 principal_arn: &str,
1954 policy_arn: &str,
1955 ) -> Result<AwsResponse, AwsServiceError> {
1956 let mut accounts = self.state.write();
1957 let state = accounts.get_or_create(&req.account_id);
1958 if !state.clusters.contains_key(cluster_name) {
1959 return Err(not_found_cluster(cluster_name)());
1960 }
1961 let entry = state
1962 .access_entries
1963 .get_mut(cluster_name)
1964 .and_then(|m| m.get_mut(principal_arn))
1965 .ok_or_else(not_found_access_entry(principal_arn))?;
1966 entry
1967 .associated_policies
1968 .retain(|ap| ap.policy_arn != policy_arn);
1969 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1970 }
1971
1972 fn list_associated_access_policies(
1973 &self,
1974 req: &AwsRequest,
1975 cluster_name: &str,
1976 principal_arn: &str,
1977 ) -> Result<AwsResponse, AwsServiceError> {
1978 let max_results = validate_max_results(req)?;
1979 let next_token = req.query_params.get("nextToken").cloned();
1980 let accounts = self.state.read();
1981 let state = accounts
1982 .get(&req.account_id)
1983 .ok_or_else(not_found_cluster(cluster_name))?;
1984 if !state.clusters.contains_key(cluster_name) {
1985 return Err(not_found_cluster(cluster_name)());
1986 }
1987 let entry = state
1988 .access_entries
1989 .get(cluster_name)
1990 .and_then(|m| m.get(principal_arn))
1991 .ok_or_else(not_found_access_entry(principal_arn))?;
1992 let policies: Vec<Value> = entry
1993 .associated_policies
1994 .iter()
1995 .map(associated_policy_json)
1996 .collect();
1997 let (page, token) = paginate_checked(&policies, next_token.as_deref(), max_results)
1998 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
1999 let mut out = json!({
2000 "clusterName": cluster_name,
2001 "principalArn": entry.principal_arn,
2002 "associatedAccessPolicies": page,
2003 });
2004 if let Some(t) = token {
2005 out["nextToken"] = Value::String(t);
2006 }
2007 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2008 }
2009
2010 fn list_access_policies(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2011 let max_results = validate_max_results(req)?;
2017 let next_token = req.query_params.get("nextToken").cloned();
2018 let catalog = access_policy_catalog();
2019 let (page, token) = paginate_checked(&catalog, next_token.as_deref(), max_results)
2020 .unwrap_or_else(|_| (catalog.clone(), None));
2021 let mut out = json!({ "accessPolicies": page });
2022 if let Some(t) = token {
2023 out["nextToken"] = Value::String(t);
2024 }
2025 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2026 }
2027
2028 fn associate_identity_provider_config(
2033 &self,
2034 req: &AwsRequest,
2035 cluster_name: &str,
2036 ) -> Result<AwsResponse, AwsServiceError> {
2037 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2038 let oidc = body
2039 .get("oidc")
2040 .filter(|v| v.is_object())
2041 .ok_or_else(|| invalid_parameter("oidc is required"))?;
2042 let name = oidc
2043 .get("identityProviderConfigName")
2044 .and_then(|v| v.as_str())
2045 .ok_or_else(|| invalid_parameter("oidc.identityProviderConfigName is required"))?
2046 .to_string();
2047 let issuer_url = oidc
2048 .get("issuerUrl")
2049 .and_then(|v| v.as_str())
2050 .ok_or_else(|| invalid_parameter("oidc.issuerUrl is required"))?
2051 .to_string();
2052 let client_id = oidc
2053 .get("clientId")
2054 .and_then(|v| v.as_str())
2055 .ok_or_else(|| invalid_parameter("oidc.clientId is required"))?
2056 .to_string();
2057
2058 let region = req.region.clone();
2059 let account_id = req.account_id.clone();
2060
2061 let mut accounts = self.state.write();
2062 let state = accounts.get_or_create(&req.account_id);
2063 if !state.clusters.contains_key(cluster_name) {
2065 return Err(not_found_cluster(cluster_name)());
2066 }
2067 if state
2068 .identity_provider_configs
2069 .get(cluster_name)
2070 .is_some_and(|m| m.contains_key(&name))
2071 {
2072 return Err(AwsServiceError::aws_error(
2073 StatusCode::CONFLICT,
2074 "ResourceInUseException",
2075 format!(
2076 "Identity provider config already exists with name {name} and cluster name {cluster_name}"
2077 ),
2078 ));
2079 }
2080
2081 let id = uuid::Uuid::new_v4().to_string();
2082 let arn = identity_provider_config_arn(®ion, &account_id, cluster_name, &name, &id);
2083 let config = IdentityProviderConfig {
2084 name: name.clone(),
2085 arn,
2086 cluster_name: cluster_name.to_string(),
2087 issuer_url,
2088 client_id,
2089 username_claim: str_field(oidc, "usernameClaim"),
2090 username_prefix: str_field(oidc, "usernamePrefix"),
2091 groups_claim: str_field(oidc, "groupsClaim"),
2092 groups_prefix: str_field(oidc, "groupsPrefix"),
2093 required_claims: oidc
2094 .get("requiredClaims")
2095 .cloned()
2096 .unwrap_or_else(|| json!({})),
2097 status: "CREATING".to_string(),
2098 tags: parse_tag_map(body.get("tags")),
2099 };
2100 state
2101 .identity_provider_configs
2102 .entry(cluster_name.to_string())
2103 .or_default()
2104 .insert(name, config.clone());
2105
2106 let cluster = state.clusters.get_mut(cluster_name).unwrap();
2109 let update = new_update(
2110 "AssociateIdentityProviderConfig",
2111 vec![("IdentityProviderConfig".to_string(), oidc.to_string())],
2112 );
2113 let update_out = update_json(&update);
2114 cluster.updates.insert(update.id.clone(), update);
2115 Ok(AwsResponse::json(
2116 StatusCode::OK,
2117 json!({ "update": update_out, "tags": config.tags }).to_string(),
2118 ))
2119 }
2120
2121 fn disassociate_identity_provider_config(
2122 &self,
2123 req: &AwsRequest,
2124 cluster_name: &str,
2125 ) -> Result<AwsResponse, AwsServiceError> {
2126 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2127 let name = body
2128 .get("identityProviderConfig")
2129 .and_then(|v| v.get("name"))
2130 .and_then(|v| v.as_str())
2131 .ok_or_else(|| invalid_parameter("identityProviderConfig.name is required"))?
2132 .to_string();
2133
2134 let mut accounts = self.state.write();
2135 let state = accounts.get_or_create(&req.account_id);
2136 if !state.clusters.contains_key(cluster_name) {
2137 return Err(not_found_cluster(cluster_name)());
2138 }
2139 state
2140 .identity_provider_configs
2141 .get_mut(cluster_name)
2142 .and_then(|m| m.remove(&name))
2143 .ok_or_else(not_found_identity_provider_config(&name))?;
2144
2145 let cluster = state.clusters.get_mut(cluster_name).unwrap();
2146 let update = new_update(
2147 "DisassociateIdentityProviderConfig",
2148 vec![("IdentityProviderConfig".to_string(), name)],
2149 );
2150 let update_out = update_json(&update);
2151 cluster.updates.insert(update.id.clone(), update);
2152 Ok(AwsResponse::json(
2153 StatusCode::OK,
2154 json!({ "update": update_out }).to_string(),
2155 ))
2156 }
2157
2158 fn describe_identity_provider_config(
2159 &self,
2160 req: &AwsRequest,
2161 cluster_name: &str,
2162 ) -> Result<AwsResponse, AwsServiceError> {
2163 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2164 let name = body
2165 .get("identityProviderConfig")
2166 .and_then(|v| v.get("name"))
2167 .and_then(|v| v.as_str())
2168 .ok_or_else(|| invalid_parameter("identityProviderConfig.name is required"))?
2169 .to_string();
2170
2171 let mut accounts = self.state.write();
2172 let state = accounts.get_or_create(&req.account_id);
2173 if !state.clusters.contains_key(cluster_name) {
2174 return Err(not_found_cluster(cluster_name)());
2175 }
2176 let config = state
2177 .identity_provider_configs
2178 .get_mut(cluster_name)
2179 .and_then(|m| m.get_mut(&name))
2180 .ok_or_else(not_found_identity_provider_config(&name))?;
2181 if config.status == "CREATING" {
2183 config.status = "ACTIVE".to_string();
2184 }
2185 Ok(AwsResponse::json(
2186 StatusCode::OK,
2187 json!({ "identityProviderConfig": identity_provider_config_json(config) }).to_string(),
2188 ))
2189 }
2190
2191 fn list_identity_provider_configs(
2192 &self,
2193 req: &AwsRequest,
2194 cluster_name: &str,
2195 ) -> Result<AwsResponse, AwsServiceError> {
2196 let max_results = validate_max_results(req)?;
2197 let next_token = req.query_params.get("nextToken").cloned();
2198 let accounts = self.state.read();
2199 let state = accounts
2200 .get(&req.account_id)
2201 .ok_or_else(not_found_cluster(cluster_name))?;
2202 if !state.clusters.contains_key(cluster_name) {
2203 return Err(not_found_cluster(cluster_name)());
2204 }
2205 let configs: Vec<Value> = state
2206 .identity_provider_configs
2207 .get(cluster_name)
2208 .map(|m| {
2209 m.keys()
2210 .map(|n| json!({ "type": "oidc", "name": n }))
2211 .collect()
2212 })
2213 .unwrap_or_default();
2214 let (page, token) = paginate_checked(&configs, next_token.as_deref(), max_results)
2215 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
2216 let mut out = json!({ "identityProviderConfigs": page });
2217 if let Some(t) = token {
2218 out["nextToken"] = Value::String(t);
2219 }
2220 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2221 }
2222
2223 fn create_pod_identity_association(
2228 &self,
2229 req: &AwsRequest,
2230 cluster_name: &str,
2231 ) -> Result<AwsResponse, AwsServiceError> {
2232 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2233 let namespace = body
2234 .get("namespace")
2235 .and_then(|v| v.as_str())
2236 .ok_or_else(|| invalid_parameter("namespace is required"))?
2237 .to_string();
2238 let service_account = body
2239 .get("serviceAccount")
2240 .and_then(|v| v.as_str())
2241 .ok_or_else(|| invalid_parameter("serviceAccount is required"))?
2242 .to_string();
2243 let role_arn = body
2244 .get("roleArn")
2245 .and_then(|v| v.as_str())
2246 .ok_or_else(|| invalid_parameter("roleArn is required"))?
2247 .to_string();
2248
2249 let region = req.region.clone();
2250 let account_id = req.account_id.clone();
2251
2252 let mut accounts = self.state.write();
2253 let state = accounts.get_or_create(&req.account_id);
2254 if !state.clusters.contains_key(cluster_name) {
2256 return Err(not_found_cluster(cluster_name)());
2257 }
2258 if state
2261 .pod_identity_associations
2262 .get(cluster_name)
2263 .is_some_and(|m| {
2264 m.values()
2265 .any(|a| a.namespace == namespace && a.service_account == service_account)
2266 })
2267 {
2268 return Err(AwsServiceError::aws_error(
2269 StatusCode::CONFLICT,
2270 "ResourceInUseException",
2271 format!(
2272 "Association already exists for namespace {namespace} and service account {service_account}"
2273 ),
2274 ));
2275 }
2276
2277 let suffix = uuid::Uuid::new_v4().to_string().replace('-', "");
2278 let suffix = &suffix[..17.min(suffix.len())];
2279 let association_id = format!("a-{suffix}");
2280 let association_arn =
2281 pod_identity_association_arn(®ion, &account_id, cluster_name, suffix);
2282 let target_role_arn = str_field(&body, "targetRoleArn");
2283 let external_id = target_role_arn
2286 .as_ref()
2287 .map(|_| uuid::Uuid::new_v4().to_string().replace('-', ""));
2288 let now = Utc::now();
2289 let assoc = PodIdentityAssociation {
2290 cluster_name: cluster_name.to_string(),
2291 namespace,
2292 service_account,
2293 role_arn,
2294 association_arn,
2295 association_id: association_id.clone(),
2296 created_at: now,
2297 modified_at: now,
2298 disable_session_tags: body
2299 .get("disableSessionTags")
2300 .and_then(|v| v.as_bool())
2301 .unwrap_or(false),
2302 target_role_arn,
2303 external_id,
2304 tags: parse_tag_map(body.get("tags")),
2305 };
2306 let out = pod_identity_association_json(&assoc);
2307 state
2308 .pod_identity_associations
2309 .entry(cluster_name.to_string())
2310 .or_default()
2311 .insert(association_id, assoc);
2312 Ok(AwsResponse::json(
2313 StatusCode::OK,
2314 json!({ "association": out }).to_string(),
2315 ))
2316 }
2317
2318 fn describe_pod_identity_association(
2319 &self,
2320 req: &AwsRequest,
2321 cluster_name: &str,
2322 association_id: &str,
2323 ) -> Result<AwsResponse, AwsServiceError> {
2324 let accounts = self.state.read();
2325 let state = accounts
2326 .get(&req.account_id)
2327 .ok_or_else(not_found_cluster(cluster_name))?;
2328 if !state.clusters.contains_key(cluster_name) {
2329 return Err(not_found_cluster(cluster_name)());
2330 }
2331 let assoc = state
2332 .pod_identity_associations
2333 .get(cluster_name)
2334 .and_then(|m| m.get(association_id))
2335 .ok_or_else(not_found_pod_identity_association(association_id))?;
2336 Ok(AwsResponse::json(
2337 StatusCode::OK,
2338 json!({ "association": pod_identity_association_json(assoc) }).to_string(),
2339 ))
2340 }
2341
2342 fn list_pod_identity_associations(
2343 &self,
2344 req: &AwsRequest,
2345 cluster_name: &str,
2346 ) -> Result<AwsResponse, AwsServiceError> {
2347 let max_results = validate_max_results(req)?;
2348 let next_token = req.query_params.get("nextToken").cloned();
2349 let namespace = req.query_params.get("namespace").cloned();
2350 let service_account = req.query_params.get("serviceAccount").cloned();
2351 let accounts = self.state.read();
2352 let state = accounts
2353 .get(&req.account_id)
2354 .ok_or_else(not_found_cluster(cluster_name))?;
2355 if !state.clusters.contains_key(cluster_name) {
2356 return Err(not_found_cluster(cluster_name)());
2357 }
2358 let summaries: Vec<Value> = state
2359 .pod_identity_associations
2360 .get(cluster_name)
2361 .map(|m| {
2362 m.values()
2363 .filter(|a| namespace.as_deref().is_none_or(|n| a.namespace == n))
2364 .filter(|a| {
2365 service_account
2366 .as_deref()
2367 .is_none_or(|s| a.service_account == s)
2368 })
2369 .map(pod_identity_association_summary_json)
2370 .collect()
2371 })
2372 .unwrap_or_default();
2373 let (page, token) = paginate_checked(&summaries, next_token.as_deref(), max_results)
2374 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
2375 let mut out = json!({ "associations": page });
2376 if let Some(t) = token {
2377 out["nextToken"] = Value::String(t);
2378 }
2379 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2380 }
2381
2382 fn delete_pod_identity_association(
2383 &self,
2384 req: &AwsRequest,
2385 cluster_name: &str,
2386 association_id: &str,
2387 ) -> Result<AwsResponse, AwsServiceError> {
2388 let mut accounts = self.state.write();
2389 let state = accounts.get_or_create(&req.account_id);
2390 if !state.clusters.contains_key(cluster_name) {
2391 return Err(not_found_cluster(cluster_name)());
2392 }
2393 let assoc = state
2394 .pod_identity_associations
2395 .get_mut(cluster_name)
2396 .and_then(|m| m.remove(association_id))
2397 .ok_or_else(not_found_pod_identity_association(association_id))?;
2398 Ok(AwsResponse::json(
2399 StatusCode::OK,
2400 json!({ "association": pod_identity_association_json(&assoc) }).to_string(),
2401 ))
2402 }
2403
2404 fn update_pod_identity_association(
2405 &self,
2406 req: &AwsRequest,
2407 cluster_name: &str,
2408 association_id: &str,
2409 ) -> Result<AwsResponse, AwsServiceError> {
2410 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2411 let mut accounts = self.state.write();
2412 let state = accounts.get_or_create(&req.account_id);
2413 if !state.clusters.contains_key(cluster_name) {
2414 return Err(not_found_cluster(cluster_name)());
2415 }
2416 let assoc = state
2417 .pod_identity_associations
2418 .get_mut(cluster_name)
2419 .and_then(|m| m.get_mut(association_id))
2420 .ok_or_else(not_found_pod_identity_association(association_id))?;
2421
2422 if let Some(role) = body.get("roleArn").and_then(|v| v.as_str()) {
2423 assoc.role_arn = role.to_string();
2424 }
2425 if let Some(target) = body.get("targetRoleArn").and_then(|v| v.as_str()) {
2426 assoc.target_role_arn = Some(target.to_string());
2427 if assoc.external_id.is_none() {
2429 assoc.external_id = Some(uuid::Uuid::new_v4().to_string().replace('-', ""));
2430 }
2431 }
2432 if let Some(disable) = body.get("disableSessionTags").and_then(|v| v.as_bool()) {
2433 assoc.disable_session_tags = disable;
2434 }
2435 assoc.modified_at = Utc::now();
2436 Ok(AwsResponse::json(
2437 StatusCode::OK,
2438 json!({ "association": pod_identity_association_json(assoc) }).to_string(),
2439 ))
2440 }
2441
2442 fn list_insights(
2447 &self,
2448 req: &AwsRequest,
2449 cluster_name: &str,
2450 ) -> Result<AwsResponse, AwsServiceError> {
2451 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2453 let max_results = match body.get("maxResults") {
2456 Some(v) => {
2457 let n = v
2458 .as_i64()
2459 .ok_or_else(|| invalid_parameter("maxResults must be an integer"))?;
2460 if !(1..=100).contains(&n) {
2461 return Err(invalid_parameter("maxResults must be between 1 and 100"));
2462 }
2463 n as usize
2464 }
2465 None => 100,
2466 };
2467 let next_token = body
2468 .get("nextToken")
2469 .and_then(|v| v.as_str())
2470 .map(|s| s.to_string());
2471 let categories = string_list(body.get("filter").and_then(|f| f.get("categories")));
2473 let statuses = string_list(body.get("filter").and_then(|f| f.get("statuses")));
2474
2475 let mut accounts = self.state.write();
2476 let state = accounts.get_or_create(&req.account_id);
2477 let version = state
2478 .clusters
2479 .get(cluster_name)
2480 .ok_or_else(not_found_cluster(cluster_name))?
2481 .version
2482 .clone();
2483 let insights = state
2484 .insights
2485 .entry(cluster_name.to_string())
2486 .or_insert_with(|| {
2487 default_insights(&version)
2488 .into_iter()
2489 .map(|i| (i.id.clone(), i))
2490 .collect()
2491 });
2492 let summaries: Vec<Value> = insights
2493 .values()
2494 .filter(|i| categories.is_empty() || categories.iter().any(|c| c == &i.category))
2495 .filter(|i| statuses.is_empty() || statuses.iter().any(|s| s == &i.status))
2496 .map(insight_summary_json)
2497 .collect();
2498 let (page, token) = paginate_checked(&summaries, next_token.as_deref(), max_results)
2499 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
2500 let mut out = json!({ "insights": page });
2501 if let Some(t) = token {
2502 out["nextToken"] = Value::String(t);
2503 }
2504 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2505 }
2506
2507 fn describe_insight(
2508 &self,
2509 req: &AwsRequest,
2510 cluster_name: &str,
2511 id: &str,
2512 ) -> Result<AwsResponse, AwsServiceError> {
2513 let mut accounts = self.state.write();
2514 let state = accounts.get_or_create(&req.account_id);
2515 let version = state
2516 .clusters
2517 .get(cluster_name)
2518 .ok_or_else(not_found_cluster(cluster_name))?
2519 .version
2520 .clone();
2521 let insights = state
2522 .insights
2523 .entry(cluster_name.to_string())
2524 .or_insert_with(|| {
2525 default_insights(&version)
2526 .into_iter()
2527 .map(|i| (i.id.clone(), i))
2528 .collect()
2529 });
2530 let insight = insights.get(id).ok_or_else(not_found_insight(id))?;
2531 Ok(AwsResponse::json(
2532 StatusCode::OK,
2533 json!({ "insight": insight_json(insight) }).to_string(),
2534 ))
2535 }
2536
2537 fn describe_insights_refresh(
2538 &self,
2539 req: &AwsRequest,
2540 cluster_name: &str,
2541 ) -> Result<AwsResponse, AwsServiceError> {
2542 let mut accounts = self.state.write();
2543 let state = accounts.get_or_create(&req.account_id);
2544 if !state.clusters.contains_key(cluster_name) {
2545 return Err(not_found_cluster(cluster_name)());
2546 }
2547 let refresh = state
2548 .insights_refresh
2549 .entry(cluster_name.to_string())
2550 .or_insert_with(|| InsightsRefresh {
2551 status: "COMPLETED".to_string(),
2552 started_at: Utc::now(),
2553 ended_at: Some(Utc::now()),
2554 });
2555 if refresh.status == "IN_PROGRESS" {
2557 refresh.status = "COMPLETED".to_string();
2558 refresh.ended_at = Some(Utc::now());
2559 }
2560 Ok(AwsResponse::json(
2561 StatusCode::OK,
2562 insights_refresh_json(refresh).to_string(),
2563 ))
2564 }
2565
2566 fn start_insights_refresh(
2567 &self,
2568 req: &AwsRequest,
2569 cluster_name: &str,
2570 ) -> Result<AwsResponse, AwsServiceError> {
2571 let mut accounts = self.state.write();
2572 let state = accounts.get_or_create(&req.account_id);
2573 let version = state
2574 .clusters
2575 .get(cluster_name)
2576 .ok_or_else(not_found_cluster(cluster_name))?
2577 .version
2578 .clone();
2579 state.insights.insert(
2581 cluster_name.to_string(),
2582 default_insights(&version)
2583 .into_iter()
2584 .map(|i| (i.id.clone(), i))
2585 .collect(),
2586 );
2587 let refresh = InsightsRefresh {
2588 status: "IN_PROGRESS".to_string(),
2589 started_at: Utc::now(),
2590 ended_at: None,
2591 };
2592 let out = json!({
2595 "message": "Insights refresh started for the cluster.",
2596 "status": refresh.status,
2597 });
2598 state
2599 .insights_refresh
2600 .insert(cluster_name.to_string(), refresh);
2601 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2602 }
2603
2604 fn associate_encryption_config(
2610 &self,
2611 req: &AwsRequest,
2612 cluster_name: &str,
2613 ) -> Result<AwsResponse, AwsServiceError> {
2614 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2615 let encryption_config = body
2616 .get("encryptionConfig")
2617 .filter(|v| v.is_array())
2618 .cloned()
2619 .ok_or_else(|| invalid_parameter("encryptionConfig is required"))?;
2620
2621 let mut accounts = self.state.write();
2622 let state = accounts.get_or_create(&req.account_id);
2623 let cluster = state
2624 .clusters
2625 .get_mut(cluster_name)
2626 .ok_or_else(not_found_cluster(cluster_name))?;
2627 cluster.encryption_config = Some(encryption_config.clone());
2628
2629 let update = new_update(
2630 "AssociateEncryptionConfig",
2631 vec![(
2632 "EncryptionConfig".to_string(),
2633 encryption_config.to_string(),
2634 )],
2635 );
2636 let out = update_json(&update);
2637 cluster.updates.insert(update.id.clone(), update);
2638 Ok(AwsResponse::json(
2639 StatusCode::OK,
2640 json!({ "update": out }).to_string(),
2641 ))
2642 }
2643
2644 fn cancel_update(
2645 &self,
2646 req: &AwsRequest,
2647 name: &str,
2648 update_id: &str,
2649 ) -> Result<AwsResponse, AwsServiceError> {
2650 let mut accounts = self.state.write();
2651 let state = accounts.get_or_create(&req.account_id);
2652 let cluster = state
2653 .clusters
2654 .get_mut(name)
2655 .ok_or_else(not_found_cluster(name))?;
2656 let update = cluster
2657 .updates
2658 .get_mut(update_id)
2659 .ok_or_else(not_found_update(update_id))?;
2660 update.status = "Cancelled".to_string();
2661 Ok(AwsResponse::json(
2662 StatusCode::OK,
2663 json!({ "update": update_json(update) }).to_string(),
2664 ))
2665 }
2666
2667 fn register_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2668 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2669 let name = body
2670 .get("name")
2671 .and_then(|v| v.as_str())
2672 .ok_or_else(|| invalid_parameter("name is required"))?
2673 .to_string();
2674 validate_cluster_name(&name)?;
2675 let connector = body
2676 .get("connectorConfig")
2677 .filter(|v| v.is_object())
2678 .ok_or_else(|| invalid_parameter("connectorConfig is required"))?;
2679 let role_arn = connector
2680 .get("roleArn")
2681 .and_then(|v| v.as_str())
2682 .ok_or_else(|| invalid_parameter("connectorConfig.roleArn is required"))?
2683 .to_string();
2684 let provider = connector
2685 .get("provider")
2686 .and_then(|v| v.as_str())
2687 .ok_or_else(|| invalid_parameter("connectorConfig.provider is required"))?
2688 .to_string();
2689
2690 let region = req.region.clone();
2691 let account_id = req.account_id.clone();
2692
2693 let mut accounts = self.state.write();
2694 let state = accounts.get_or_create(&req.account_id);
2695 if state.clusters.contains_key(&name) {
2696 return Err(AwsServiceError::aws_error(
2697 StatusCode::CONFLICT,
2698 "ResourceInUseException",
2699 format!("Cluster already exists with name: {name}"),
2700 ));
2701 }
2702
2703 let arn = cluster_arn(®ion, &account_id, &name);
2704 let id = uuid::Uuid::new_v4().to_string();
2705 let activation_id = uuid::Uuid::new_v4().to_string();
2706 let activation_code = uuid::Uuid::new_v4().to_string().replace('-', "");
2707 let connector_config = json!({
2708 "activationId": activation_id,
2709 "activationCode": activation_code,
2710 "activationExpiry": timestamp_to_number(Utc::now() + chrono::Duration::hours(72)),
2711 "provider": provider,
2712 "roleArn": role_arn,
2713 });
2714
2715 let cluster = Cluster {
2716 name: name.clone(),
2717 arn,
2718 version: String::new(),
2719 role_arn: String::new(),
2720 status: "PENDING".to_string(),
2721 created_at: Utc::now(),
2722 endpoint: String::new(),
2723 platform_version: "eks.1".to_string(),
2724 certificate_authority_data: String::new(),
2725 resources_vpc_config: json!({}),
2726 kubernetes_network_config: json!({}),
2727 logging: build_logging(None),
2728 tags: parse_tag_map(body.get("tags")),
2729 updates: Default::default(),
2730 connector_config: Some(connector_config),
2731 encryption_config: None,
2732 access_config: build_access_config(None),
2733 upgrade_policy: build_upgrade_policy(None),
2734 compute_config: None,
2735 storage_config: None,
2736 zonal_shift_config: None,
2737 remote_network_config: None,
2738 control_plane_scaling_config: None,
2739 deletion_protection: None,
2740 };
2741 let out = connected_cluster_json(&cluster, &id);
2742 state.clusters.insert(name, cluster);
2743 Ok(AwsResponse::json(
2744 StatusCode::OK,
2745 json!({ "cluster": out }).to_string(),
2746 ))
2747 }
2748
2749 fn deregister_cluster(
2750 &self,
2751 req: &AwsRequest,
2752 name: &str,
2753 ) -> Result<AwsResponse, AwsServiceError> {
2754 let mut accounts = self.state.write();
2755 let state = accounts.get_or_create(&req.account_id);
2756 let is_connected = state
2758 .clusters
2759 .get(name)
2760 .map(|c| c.connector_config.is_some())
2761 .unwrap_or(false);
2762 if !is_connected {
2763 return Err(not_found_cluster(name)());
2764 }
2765 let mut cluster = state.clusters.remove(name).unwrap();
2766 cluster.status = "DELETING".to_string();
2767 let id = uuid::Uuid::new_v4().to_string();
2768 Ok(AwsResponse::json(
2769 StatusCode::OK,
2770 json!({ "cluster": connected_cluster_json(&cluster, &id) }).to_string(),
2771 ))
2772 }
2773
2774 fn describe_cluster_versions(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2775 let next_token = req.query_params.get("nextToken").cloned();
2776 let max_results = match req.query_params.get("maxResults") {
2777 Some(raw) => {
2778 let n: i64 = raw
2779 .parse()
2780 .map_err(|_| invalid_parameter("maxResults must be an integer"))?;
2781 if !(1..=100).contains(&n) {
2782 return Err(invalid_parameter("maxResults must be between 1 and 100"));
2783 }
2784 n as usize
2785 }
2786 None => 100,
2787 };
2788 if let Some(status) = req.query_params.get("status") {
2790 if !["unsupported", "standard_support", "extended_support"].contains(&status.as_str()) {
2791 return Err(invalid_parameter(format!("Invalid status: {status}")));
2792 }
2793 }
2794 if let Some(vs) = req.query_params.get("versionStatus") {
2795 if !["UNSUPPORTED", "STANDARD_SUPPORT", "EXTENDED_SUPPORT"].contains(&vs.as_str()) {
2796 return Err(invalid_parameter(format!("Invalid versionStatus: {vs}")));
2797 }
2798 }
2799 let default_only = req
2800 .query_params
2801 .get("defaultOnly")
2802 .map(|v| v == "true")
2803 .unwrap_or(false);
2804 let include_all = req
2807 .query_params
2808 .get("includeAll")
2809 .map(|v| v == "true")
2810 .unwrap_or(false);
2811 let status_filter = req.query_params.get("status").cloned();
2812 let version_status_filter = req.query_params.get("versionStatus").cloned();
2813 let version_filter = parse_multi_query(&req.raw_query, "clusterVersions");
2814 let cluster_type = req
2815 .query_params
2816 .get("clusterType")
2817 .cloned()
2818 .unwrap_or_else(|| "eks".to_string());
2819
2820 let mut catalog: Vec<Value> = cluster_version_catalog(&cluster_type)
2821 .into_iter()
2822 .filter(|v| {
2823 version_filter.is_empty()
2824 || version_filter
2825 .iter()
2826 .any(|f| v["clusterVersion"] == f.as_str())
2827 })
2828 .filter(|v| !default_only || v["defaultVersion"] == true)
2829 .filter(|v| status_filter.as_deref().is_none_or(|s| v["status"] == s))
2832 .filter(|v| {
2833 version_status_filter
2834 .as_deref()
2835 .is_none_or(|s| v["versionStatus"] == s)
2836 })
2837 .filter(|v| {
2840 include_all
2841 || status_filter.is_some()
2842 || version_status_filter.is_some()
2843 || v["versionStatus"] != "UNSUPPORTED"
2844 })
2845 .collect();
2846 catalog.sort_by_key(|v| v["defaultVersion"] != true);
2849
2850 let (page, token) = paginate_checked(&catalog, next_token.as_deref(), max_results)
2851 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
2852 let mut out = json!({ "clusterVersions": page });
2853 if let Some(t) = token {
2854 out["nextToken"] = Value::String(t);
2855 }
2856 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2857 }
2858
2859 fn create_capability(
2864 &self,
2865 req: &AwsRequest,
2866 cluster_name: &str,
2867 ) -> Result<AwsResponse, AwsServiceError> {
2868 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
2869 let name = body
2870 .get("capabilityName")
2871 .and_then(|v| v.as_str())
2872 .ok_or_else(|| invalid_parameter("capabilityName is required"))?
2873 .to_string();
2874 let type_ = body
2875 .get("type")
2876 .and_then(|v| v.as_str())
2877 .ok_or_else(|| invalid_parameter("type is required"))?
2878 .to_string();
2879 let role_arn = body
2880 .get("roleArn")
2881 .and_then(|v| v.as_str())
2882 .ok_or_else(|| invalid_parameter("roleArn is required"))?
2883 .to_string();
2884
2885 let region = req.region.clone();
2886 let account_id = req.account_id.clone();
2887
2888 let mut accounts = self.state.write();
2889 let state = accounts.get_or_create(&req.account_id);
2890 if !state.clusters.contains_key(cluster_name) {
2891 return Err(not_found_cluster(cluster_name)());
2892 }
2893 if state
2894 .capabilities
2895 .get(cluster_name)
2896 .is_some_and(|m| m.contains_key(&name))
2897 {
2898 return Err(AwsServiceError::aws_error(
2899 StatusCode::CONFLICT,
2900 "ResourceInUseException",
2901 format!(
2902 "Capability already exists with name {name} and cluster name {cluster_name}"
2903 ),
2904 ));
2905 }
2906
2907 let id = uuid::Uuid::new_v4().to_string();
2908 let arn = capability_arn(®ion, &account_id, cluster_name, &name, &id);
2909 let now = Utc::now();
2910 let capability = Capability {
2911 name: name.clone(),
2912 arn,
2913 cluster_name: cluster_name.to_string(),
2914 type_,
2915 role_arn,
2916 status: "CREATING".to_string(),
2917 version: "v1".to_string(),
2918 configuration: normalize_capability_configuration(body.get("configuration")),
2919 tags: parse_tag_map(body.get("tags")),
2920 created_at: now,
2921 modified_at: now,
2922 delete_propagation_policy: str_field(&body, "deletePropagationPolicy"),
2923 };
2924 let out = capability_json(&capability);
2925 state
2926 .capabilities
2927 .entry(cluster_name.to_string())
2928 .or_default()
2929 .insert(name, capability);
2930 Ok(AwsResponse::json(
2931 StatusCode::OK,
2932 json!({ "capability": out }).to_string(),
2933 ))
2934 }
2935
2936 fn describe_capability(
2937 &self,
2938 req: &AwsRequest,
2939 cluster_name: &str,
2940 name: &str,
2941 ) -> Result<AwsResponse, AwsServiceError> {
2942 let mut accounts = self.state.write();
2943 let state = accounts.get_or_create(&req.account_id);
2944 if !state.clusters.contains_key(cluster_name) {
2945 return Err(not_found_cluster(cluster_name)());
2946 }
2947 let capability = state
2948 .capabilities
2949 .get_mut(cluster_name)
2950 .and_then(|m| m.get_mut(name))
2951 .ok_or_else(not_found_capability(name))?;
2952 if capability.status == "CREATING" {
2953 capability.status = "ACTIVE".to_string();
2954 }
2955 Ok(AwsResponse::json(
2956 StatusCode::OK,
2957 json!({ "capability": capability_json(capability) }).to_string(),
2958 ))
2959 }
2960
2961 fn list_capabilities(
2962 &self,
2963 req: &AwsRequest,
2964 cluster_name: &str,
2965 ) -> Result<AwsResponse, AwsServiceError> {
2966 let max_results = validate_max_results(req)?;
2967 let next_token = req.query_params.get("nextToken").cloned();
2968 let accounts = self.state.read();
2969 let state = accounts
2970 .get(&req.account_id)
2971 .ok_or_else(not_found_cluster(cluster_name))?;
2972 if !state.clusters.contains_key(cluster_name) {
2973 return Err(not_found_cluster(cluster_name)());
2974 }
2975 let summaries: Vec<Value> = state
2976 .capabilities
2977 .get(cluster_name)
2978 .map(|m| m.values().map(capability_summary_json).collect())
2979 .unwrap_or_default();
2980 let (page, token) = paginate_checked(&summaries, next_token.as_deref(), max_results)
2981 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
2982 let mut out = json!({ "capabilities": page });
2983 if let Some(t) = token {
2984 out["nextToken"] = Value::String(t);
2985 }
2986 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
2987 }
2988
2989 fn delete_capability(
2990 &self,
2991 req: &AwsRequest,
2992 cluster_name: &str,
2993 name: &str,
2994 ) -> Result<AwsResponse, AwsServiceError> {
2995 let mut accounts = self.state.write();
2996 let state = accounts.get_or_create(&req.account_id);
2997 if !state.clusters.contains_key(cluster_name) {
2998 return Err(not_found_cluster(cluster_name)());
2999 }
3000 let mut capability = state
3001 .capabilities
3002 .get_mut(cluster_name)
3003 .and_then(|m| m.remove(name))
3004 .ok_or_else(not_found_capability(name))?;
3005 capability.status = "DELETING".to_string();
3006 Ok(AwsResponse::json(
3007 StatusCode::OK,
3008 json!({ "capability": capability_json(&capability) }).to_string(),
3009 ))
3010 }
3011
3012 fn update_capability(
3013 &self,
3014 req: &AwsRequest,
3015 cluster_name: &str,
3016 name: &str,
3017 ) -> Result<AwsResponse, AwsServiceError> {
3018 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
3019 let mut accounts = self.state.write();
3020 let state = accounts.get_or_create(&req.account_id);
3021 if !state.clusters.contains_key(cluster_name) {
3022 return Err(not_found_cluster(cluster_name)());
3023 }
3024 let capability = state
3025 .capabilities
3026 .get_mut(cluster_name)
3027 .and_then(|m| m.get_mut(name))
3028 .ok_or_else(not_found_capability(name))?;
3029
3030 let mut params = Vec::new();
3031 if let Some(role) = body.get("roleArn").and_then(|v| v.as_str()) {
3032 capability.role_arn = role.to_string();
3033 params.push(("RoleArn".to_string(), role.to_string()));
3034 }
3035 if let Some(cfg) = normalize_capability_configuration(body.get("configuration")) {
3036 params.push(("Configuration".to_string(), cfg.to_string()));
3037 capability.configuration = Some(cfg);
3038 }
3039 capability.modified_at = Utc::now();
3040
3041 let update = new_update("CapabilityUpdate", params);
3043 let out = update_json(&update);
3044 let cluster = state.clusters.get_mut(cluster_name).unwrap();
3045 cluster.updates.insert(update.id.clone(), update);
3046 Ok(AwsResponse::json(
3047 StatusCode::OK,
3048 json!({ "update": out }).to_string(),
3049 ))
3050 }
3051
3052 fn create_eks_anywhere_subscription(
3057 &self,
3058 req: &AwsRequest,
3059 ) -> Result<AwsResponse, AwsServiceError> {
3060 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
3061 let name = body
3062 .get("name")
3063 .and_then(|v| v.as_str())
3064 .ok_or_else(|| invalid_parameter("name is required"))?
3065 .to_string();
3066 if name.is_empty() || name.len() > 100 {
3068 return Err(invalid_parameter("name must be 1-100 characters"));
3069 }
3070 if !name.starts_with(|c: char| c.is_ascii_alphanumeric())
3071 || !name
3072 .chars()
3073 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
3074 {
3075 return Err(invalid_parameter(
3076 "name must match ^[0-9A-Za-z][A-Za-z0-9\\-_]*$",
3077 ));
3078 }
3079 if let Some(lt) = body.get("licenseType").and_then(|v| v.as_str()) {
3080 if lt != "Cluster" {
3081 return Err(invalid_parameter(format!("Invalid licenseType: {lt}")));
3082 }
3083 }
3084 let term = body
3085 .get("term")
3086 .filter(|v| v.is_object())
3087 .ok_or_else(|| invalid_parameter("term is required"))?;
3088 let term_duration = term.get("duration").and_then(|v| v.as_i64()).unwrap_or(12);
3089 let term_unit = term
3090 .get("unit")
3091 .and_then(|v| v.as_str())
3092 .unwrap_or("MONTHS")
3093 .to_string();
3094 let license_quantity = body
3095 .get("licenseQuantity")
3096 .and_then(|v| v.as_i64())
3097 .unwrap_or(0);
3098 let license_type = body
3099 .get("licenseType")
3100 .and_then(|v| v.as_str())
3101 .unwrap_or("Cluster")
3102 .to_string();
3103 let auto_renew = body
3104 .get("autoRenew")
3105 .and_then(|v| v.as_bool())
3106 .unwrap_or(false);
3107
3108 let region = req.region.clone();
3109 let account_id = req.account_id.clone();
3110
3111 let mut accounts = self.state.write();
3112 let state = accounts.get_or_create(&req.account_id);
3113
3114 let raw = uuid::Uuid::new_v4().to_string().replace('-', "");
3115 let id = raw[..17.min(raw.len())].to_string();
3116 let arn = eks_anywhere_subscription_arn(®ion, &account_id, &id);
3117 let now = Utc::now();
3118 let expiration = now + chrono::Duration::days(term_duration * 30);
3119 let subscription = EksAnywhereSubscription {
3120 id: id.clone(),
3121 arn,
3122 name,
3123 created_at: now,
3124 effective_date: now,
3125 expiration_date: expiration,
3126 license_quantity,
3127 license_type,
3128 term_duration,
3129 term_unit,
3130 status: "ACTIVE".to_string(),
3131 auto_renew,
3132 tags: parse_tag_map(body.get("tags")),
3133 };
3134 let out = subscription_json(&subscription);
3135 state.eks_anywhere_subscriptions.insert(id, subscription);
3136 Ok(AwsResponse::json(
3137 StatusCode::OK,
3138 json!({ "subscription": out }).to_string(),
3139 ))
3140 }
3141
3142 fn list_eks_anywhere_subscriptions(
3143 &self,
3144 req: &AwsRequest,
3145 ) -> Result<AwsResponse, AwsServiceError> {
3146 let max_results = validate_max_results(req)?;
3147 let next_token = req.query_params.get("nextToken").cloned();
3148 let accounts = self.state.read();
3149 let Some(state) = accounts.get(&req.account_id) else {
3150 return Ok(AwsResponse::json(
3151 StatusCode::OK,
3152 json!({ "subscriptions": [] }).to_string(),
3153 ));
3154 };
3155 let subs: Vec<Value> = state
3156 .eks_anywhere_subscriptions
3157 .values()
3158 .map(subscription_json)
3159 .collect();
3160 let (page, token) = paginate_checked(&subs, next_token.as_deref(), max_results)
3161 .map_err(|_| invalid_parameter("Invalid nextToken"))?;
3162 let mut out = json!({ "subscriptions": page });
3163 if let Some(t) = token {
3164 out["nextToken"] = Value::String(t);
3165 }
3166 Ok(AwsResponse::json(StatusCode::OK, out.to_string()))
3167 }
3168
3169 fn describe_eks_anywhere_subscription(
3170 &self,
3171 req: &AwsRequest,
3172 id: &str,
3173 ) -> Result<AwsResponse, AwsServiceError> {
3174 let accounts = self.state.read();
3175 let state = accounts
3176 .get(&req.account_id)
3177 .ok_or_else(not_found_subscription(id))?;
3178 let subscription = state
3179 .eks_anywhere_subscriptions
3180 .get(id)
3181 .ok_or_else(not_found_subscription(id))?;
3182 Ok(AwsResponse::json(
3183 StatusCode::OK,
3184 json!({ "subscription": subscription_json(subscription) }).to_string(),
3185 ))
3186 }
3187
3188 fn delete_eks_anywhere_subscription(
3189 &self,
3190 req: &AwsRequest,
3191 id: &str,
3192 ) -> Result<AwsResponse, AwsServiceError> {
3193 let mut accounts = self.state.write();
3194 let state = accounts.get_or_create(&req.account_id);
3195 let mut subscription = state
3196 .eks_anywhere_subscriptions
3197 .remove(id)
3198 .ok_or_else(not_found_subscription(id))?;
3199 subscription.status = "DELETING".to_string();
3200 Ok(AwsResponse::json(
3201 StatusCode::OK,
3202 json!({ "subscription": subscription_json(&subscription) }).to_string(),
3203 ))
3204 }
3205
3206 fn update_eks_anywhere_subscription(
3207 &self,
3208 req: &AwsRequest,
3209 id: &str,
3210 ) -> Result<AwsResponse, AwsServiceError> {
3211 let body: Value = serde_json::from_slice(&req.body).unwrap_or_default();
3212 let auto_renew = body
3213 .get("autoRenew")
3214 .and_then(|v| v.as_bool())
3215 .ok_or_else(|| invalid_parameter("autoRenew is required"))?;
3216 let mut accounts = self.state.write();
3217 let state = accounts.get_or_create(&req.account_id);
3218 let subscription = state
3219 .eks_anywhere_subscriptions
3220 .get_mut(id)
3221 .ok_or_else(not_found_subscription(id))?;
3222 subscription.auto_renew = auto_renew;
3223 Ok(AwsResponse::json(
3224 StatusCode::OK,
3225 json!({ "subscription": subscription_json(subscription) }).to_string(),
3226 ))
3227 }
3228}
3229
3230pub async fn save_eks_snapshot(
3233 state: &SharedEksState,
3234 store: Option<Arc<dyn SnapshotStore>>,
3235 lock: &AsyncMutex<()>,
3236) {
3237 let Some(store) = store else {
3238 return;
3239 };
3240 let _guard = lock.lock().await;
3241 let snapshot = EksSnapshot {
3242 schema_version: EKS_SNAPSHOT_SCHEMA_VERSION,
3243 accounts: state.read().clone(),
3244 };
3245 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
3246 let bytes = serde_json::to_vec(&snapshot)
3247 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
3248 store.save(&bytes)
3249 })
3250 .await;
3251 match join {
3252 Ok(Ok(())) => {}
3253 Ok(Err(err)) => tracing::error!(%err, "failed to write eks snapshot"),
3254 Err(err) => tracing::error!(%err, "eks snapshot task panicked"),
3255 }
3256}
3257
3258#[async_trait]
3259impl AwsService for EksService {
3260 fn service_name(&self) -> &str {
3261 "eks"
3262 }
3263
3264 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3265 let (action, args) = Self::resolve_action(&req).ok_or_else(|| {
3266 AwsServiceError::aws_error(
3267 StatusCode::NOT_FOUND,
3268 "UnknownOperationException",
3269 format!("Unknown operation: {} {}", req.method, req.raw_path),
3270 )
3271 })?;
3272
3273 let mutates = matches!(
3274 action,
3275 "CreateCluster"
3276 | "DeleteCluster"
3277 | "UpdateClusterConfig"
3278 | "UpdateClusterVersion"
3279 | "TagResource"
3280 | "UntagResource"
3281 | "DescribeCluster"
3284 | "DescribeUpdate"
3285 | "CreateNodegroup"
3286 | "DeleteNodegroup"
3287 | "UpdateNodegroupConfig"
3288 | "UpdateNodegroupVersion"
3289 | "DescribeNodegroup"
3290 | "CreateFargateProfile"
3291 | "DeleteFargateProfile"
3292 | "DescribeFargateProfile"
3293 | "CreateAddon"
3294 | "DeleteAddon"
3295 | "UpdateAddon"
3296 | "DescribeAddon"
3297 | "CreateAccessEntry"
3298 | "DeleteAccessEntry"
3299 | "UpdateAccessEntry"
3300 | "AssociateAccessPolicy"
3301 | "DisassociateAccessPolicy"
3302 | "AssociateIdentityProviderConfig"
3303 | "DisassociateIdentityProviderConfig"
3304 | "DescribeIdentityProviderConfig"
3306 | "CreatePodIdentityAssociation"
3307 | "DeletePodIdentityAssociation"
3308 | "UpdatePodIdentityAssociation"
3309 | "ListInsights"
3311 | "DescribeInsight"
3312 | "DescribeInsightsRefresh"
3313 | "StartInsightsRefresh"
3314 | "AssociateEncryptionConfig"
3315 | "CancelUpdate"
3316 | "RegisterCluster"
3317 | "DeregisterCluster"
3318 | "CreateCapability"
3319 | "DeleteCapability"
3320 | "UpdateCapability"
3321 | "DescribeCapability"
3322 | "CreateEksAnywhereSubscription"
3323 | "DeleteEksAnywhereSubscription"
3324 | "UpdateEksAnywhereSubscription"
3325 | "DescribeEksAnywhereSubscription"
3326 );
3327
3328 let result = match (action, &args) {
3329 ("CreateCluster", _) => self.create_cluster(&req),
3330 ("ListClusters", _) => self.list_clusters(&req),
3331 ("DescribeCluster", PathArgs::Name(n)) => self.describe_cluster(&req, n),
3332 ("DeleteCluster", PathArgs::Name(n)) => self.delete_cluster(&req, n),
3333 ("UpdateClusterConfig", PathArgs::Name(n)) => self.update_cluster_config(&req, n),
3334 ("UpdateClusterVersion", PathArgs::Name(n)) => self.update_cluster_version(&req, n),
3335 ("ListUpdates", PathArgs::Name(n)) => self.list_updates(&req, n),
3336 ("DescribeUpdate", PathArgs::Update { name, update_id }) => {
3337 self.describe_update(&req, name, update_id)
3338 }
3339 ("TagResource", PathArgs::Arn(a)) => self.tag_resource(&req, a),
3340 ("UntagResource", PathArgs::Arn(a)) => self.untag_resource(&req, a),
3341 ("ListTagsForResource", PathArgs::Arn(a)) => self.list_tags_for_resource(&req, a),
3342 ("CreateNodegroup", PathArgs::Cluster(c)) => self.create_nodegroup(&req, c),
3343 ("ListNodegroups", PathArgs::Cluster(c)) => self.list_nodegroups(&req, c),
3344 ("DescribeNodegroup", PathArgs::ClusterChild { cluster, name }) => {
3345 self.describe_nodegroup(&req, cluster, name)
3346 }
3347 ("DeleteNodegroup", PathArgs::ClusterChild { cluster, name }) => {
3348 self.delete_nodegroup(&req, cluster, name)
3349 }
3350 ("UpdateNodegroupConfig", PathArgs::ClusterChild { cluster, name }) => {
3351 self.update_nodegroup_config(&req, cluster, name)
3352 }
3353 ("UpdateNodegroupVersion", PathArgs::ClusterChild { cluster, name }) => {
3354 self.update_nodegroup_version(&req, cluster, name)
3355 }
3356 ("CreateFargateProfile", PathArgs::Cluster(c)) => self.create_fargate_profile(&req, c),
3357 ("ListFargateProfiles", PathArgs::Cluster(c)) => self.list_fargate_profiles(&req, c),
3358 ("DescribeFargateProfile", PathArgs::ClusterChild { cluster, name }) => {
3359 self.describe_fargate_profile(&req, cluster, name)
3360 }
3361 ("DeleteFargateProfile", PathArgs::ClusterChild { cluster, name }) => {
3362 self.delete_fargate_profile(&req, cluster, name)
3363 }
3364 ("CreateAddon", PathArgs::Cluster(c)) => self.create_addon(&req, c),
3365 ("ListAddons", PathArgs::Cluster(c)) => self.list_addons(&req, c),
3366 ("DescribeAddon", PathArgs::ClusterChild { cluster, name }) => {
3367 self.describe_addon(&req, cluster, name)
3368 }
3369 ("DeleteAddon", PathArgs::ClusterChild { cluster, name }) => {
3370 self.delete_addon(&req, cluster, name)
3371 }
3372 ("UpdateAddon", PathArgs::ClusterChild { cluster, name }) => {
3373 self.update_addon(&req, cluster, name)
3374 }
3375 ("DescribeAddonVersions", _) => self.describe_addon_versions(&req),
3376 ("DescribeAddonConfiguration", _) => self.describe_addon_configuration(&req),
3377 ("CreateAccessEntry", PathArgs::Cluster(c)) => self.create_access_entry(&req, c),
3378 ("ListAccessEntries", PathArgs::Cluster(c)) => self.list_access_entries(&req, c),
3379 ("DescribeAccessEntry", PathArgs::ClusterChild { cluster, name }) => {
3380 self.describe_access_entry(&req, cluster, name)
3381 }
3382 ("DeleteAccessEntry", PathArgs::ClusterChild { cluster, name }) => {
3383 self.delete_access_entry(&req, cluster, name)
3384 }
3385 ("UpdateAccessEntry", PathArgs::ClusterChild { cluster, name }) => {
3386 self.update_access_entry(&req, cluster, name)
3387 }
3388 ("AssociateAccessPolicy", PathArgs::ClusterChild { cluster, name }) => {
3389 self.associate_access_policy(&req, cluster, name)
3390 }
3391 ("ListAssociatedAccessPolicies", PathArgs::ClusterChild { cluster, name }) => {
3392 self.list_associated_access_policies(&req, cluster, name)
3393 }
3394 (
3395 "DisassociateAccessPolicy",
3396 PathArgs::AccessPolicyChild {
3397 cluster,
3398 principal,
3399 policy_arn,
3400 },
3401 ) => self.disassociate_access_policy(&req, cluster, principal, policy_arn),
3402 ("ListAccessPolicies", _) => self.list_access_policies(&req),
3403 ("AssociateIdentityProviderConfig", PathArgs::Cluster(c)) => {
3404 self.associate_identity_provider_config(&req, c)
3405 }
3406 ("DisassociateIdentityProviderConfig", PathArgs::Cluster(c)) => {
3407 self.disassociate_identity_provider_config(&req, c)
3408 }
3409 ("DescribeIdentityProviderConfig", PathArgs::Cluster(c)) => {
3410 self.describe_identity_provider_config(&req, c)
3411 }
3412 ("ListIdentityProviderConfigs", PathArgs::Cluster(c)) => {
3413 self.list_identity_provider_configs(&req, c)
3414 }
3415 ("CreatePodIdentityAssociation", PathArgs::Cluster(c)) => {
3416 self.create_pod_identity_association(&req, c)
3417 }
3418 ("ListPodIdentityAssociations", PathArgs::Cluster(c)) => {
3419 self.list_pod_identity_associations(&req, c)
3420 }
3421 ("DescribePodIdentityAssociation", PathArgs::ClusterChild { cluster, name }) => {
3422 self.describe_pod_identity_association(&req, cluster, name)
3423 }
3424 ("DeletePodIdentityAssociation", PathArgs::ClusterChild { cluster, name }) => {
3425 self.delete_pod_identity_association(&req, cluster, name)
3426 }
3427 ("UpdatePodIdentityAssociation", PathArgs::ClusterChild { cluster, name }) => {
3428 self.update_pod_identity_association(&req, cluster, name)
3429 }
3430 ("ListInsights", PathArgs::Cluster(c)) => self.list_insights(&req, c),
3431 ("DescribeInsight", PathArgs::ClusterChild { cluster, name }) => {
3432 self.describe_insight(&req, cluster, name)
3433 }
3434 ("DescribeInsightsRefresh", PathArgs::Cluster(c)) => {
3435 self.describe_insights_refresh(&req, c)
3436 }
3437 ("StartInsightsRefresh", PathArgs::Cluster(c)) => self.start_insights_refresh(&req, c),
3438 ("AssociateEncryptionConfig", PathArgs::Cluster(c)) => {
3439 self.associate_encryption_config(&req, c)
3440 }
3441 ("CancelUpdate", PathArgs::Update { name, update_id }) => {
3442 self.cancel_update(&req, name, update_id)
3443 }
3444 ("RegisterCluster", _) => self.register_cluster(&req),
3445 ("DeregisterCluster", PathArgs::Name(n)) => self.deregister_cluster(&req, n),
3446 ("DescribeClusterVersions", _) => self.describe_cluster_versions(&req),
3447 ("CreateCapability", PathArgs::Cluster(c)) => self.create_capability(&req, c),
3448 ("ListCapabilities", PathArgs::Cluster(c)) => self.list_capabilities(&req, c),
3449 ("DescribeCapability", PathArgs::ClusterChild { cluster, name }) => {
3450 self.describe_capability(&req, cluster, name)
3451 }
3452 ("DeleteCapability", PathArgs::ClusterChild { cluster, name }) => {
3453 self.delete_capability(&req, cluster, name)
3454 }
3455 ("UpdateCapability", PathArgs::ClusterChild { cluster, name }) => {
3456 self.update_capability(&req, cluster, name)
3457 }
3458 ("CreateEksAnywhereSubscription", _) => self.create_eks_anywhere_subscription(&req),
3459 ("ListEksAnywhereSubscriptions", _) => self.list_eks_anywhere_subscriptions(&req),
3460 ("DescribeEksAnywhereSubscription", PathArgs::Name(id)) => {
3461 self.describe_eks_anywhere_subscription(&req, id)
3462 }
3463 ("DeleteEksAnywhereSubscription", PathArgs::Name(id)) => {
3464 self.delete_eks_anywhere_subscription(&req, id)
3465 }
3466 ("UpdateEksAnywhereSubscription", PathArgs::Name(id)) => {
3467 self.update_eks_anywhere_subscription(&req, id)
3468 }
3469 _ => Err(AwsServiceError::action_not_implemented("eks", action)),
3470 };
3471
3472 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
3473 self.save_snapshot().await;
3474 }
3475 result
3476 }
3477
3478 fn supported_actions(&self) -> &[&str] {
3479 EKS_ACTIONS
3480 }
3481}
3482
3483fn decode(s: &str) -> String {
3488 percent_encoding::percent_decode_str(s)
3489 .decode_utf8_lossy()
3490 .into_owned()
3491}
3492
3493fn invalid_parameter(msg: impl Into<String>) -> AwsServiceError {
3494 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidParameterException", msg)
3495}
3496
3497fn bad_request(msg: impl Into<String>) -> AwsServiceError {
3498 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "BadRequestException", msg)
3499}
3500
3501fn validate_max_results(req: &AwsRequest) -> Result<usize, AwsServiceError> {
3504 match req.query_params.get("maxResults") {
3505 Some(raw) => {
3506 let n: i64 = raw
3507 .parse()
3508 .map_err(|_| invalid_parameter("maxResults must be an integer"))?;
3509 if !(1..=100).contains(&n) {
3510 return Err(invalid_parameter("maxResults must be between 1 and 100"));
3511 }
3512 Ok(n as usize)
3513 }
3514 None => Ok(100),
3515 }
3516}
3517
3518fn validate_cluster_name(name: &str) -> Result<(), AwsServiceError> {
3519 if name.is_empty() || name.len() > 100 {
3520 return Err(invalid_parameter("name must be 1-100 characters"));
3521 }
3522 let mut chars = name.chars();
3523 let first = chars.next().unwrap();
3524 if !first.is_ascii_alphanumeric() {
3525 return Err(invalid_parameter(
3526 "name must start with an alphanumeric character",
3527 ));
3528 }
3529 if !name
3530 .chars()
3531 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
3532 {
3533 return Err(invalid_parameter(
3534 "name must match ^[0-9A-Za-z][A-Za-z0-9\\-_]*$",
3535 ));
3536 }
3537 Ok(())
3538}
3539
3540fn not_found_cluster(name: &str) -> impl Fn() -> AwsServiceError + 'static {
3541 let name = name.to_string();
3542 move || {
3543 AwsServiceError::aws_error(
3544 StatusCode::NOT_FOUND,
3545 "ResourceNotFoundException",
3546 format!("No cluster found for name: {name}."),
3547 )
3548 }
3549}
3550
3551fn not_found_update(id: &str) -> impl Fn() -> AwsServiceError + 'static {
3552 let id = id.to_string();
3553 move || {
3554 AwsServiceError::aws_error(
3555 StatusCode::NOT_FOUND,
3556 "ResourceNotFoundException",
3557 format!("No update found for id: {id}."),
3558 )
3559 }
3560}
3561
3562fn not_found_nodegroup(name: &str) -> impl Fn() -> AwsServiceError + 'static {
3563 let name = name.to_string();
3564 move || {
3565 AwsServiceError::aws_error(
3566 StatusCode::NOT_FOUND,
3567 "ResourceNotFoundException",
3568 format!("No node group found for name: {name}."),
3569 )
3570 }
3571}
3572
3573fn not_found_fargate_profile(name: &str) -> impl Fn() -> AwsServiceError + 'static {
3574 let name = name.to_string();
3575 move || {
3576 AwsServiceError::aws_error(
3577 StatusCode::NOT_FOUND,
3578 "ResourceNotFoundException",
3579 format!("No Fargate Profile found with name: {name}."),
3580 )
3581 }
3582}
3583
3584fn not_found_addon(name: &str) -> impl Fn() -> AwsServiceError + 'static {
3585 let name = name.to_string();
3586 move || {
3587 AwsServiceError::aws_error(
3588 StatusCode::NOT_FOUND,
3589 "ResourceNotFoundException",
3590 format!("No addon found for name: {name}."),
3591 )
3592 }
3593}
3594
3595fn not_found_access_entry(principal_arn: &str) -> impl Fn() -> AwsServiceError + 'static {
3596 let principal_arn = principal_arn.to_string();
3597 move || {
3598 AwsServiceError::aws_error(
3599 StatusCode::NOT_FOUND,
3600 "ResourceNotFoundException",
3601 format!("No access entry found for principal ARN: {principal_arn}."),
3602 )
3603 }
3604}
3605
3606fn not_found_identity_provider_config(name: &str) -> impl Fn() -> AwsServiceError + 'static {
3607 let name = name.to_string();
3608 move || {
3609 AwsServiceError::aws_error(
3610 StatusCode::NOT_FOUND,
3611 "ResourceNotFoundException",
3612 format!("No identity provider config found for name: {name}."),
3613 )
3614 }
3615}
3616
3617fn not_found_pod_identity_association(id: &str) -> impl Fn() -> AwsServiceError + 'static {
3618 let id = id.to_string();
3619 move || {
3620 AwsServiceError::aws_error(
3621 StatusCode::NOT_FOUND,
3622 "ResourceNotFoundException",
3623 format!("No pod identity association found for id: {id}."),
3624 )
3625 }
3626}
3627
3628fn validate_eks_arn(arn: &str) -> Result<(), AwsServiceError> {
3631 let parts: Vec<&str> = arn.split(':').collect();
3632 if parts.len() < 6 || parts[0] != "arn" || parts[2] != "eks" {
3633 return Err(bad_request(format!("Invalid EKS ARN: {arn}")));
3634 }
3635 Ok(())
3636}
3637
3638enum TagTarget {
3642 Cluster(String),
3643 Nodegroup(String, String),
3644 Fargate(String, String),
3645 Addon(String, String),
3646 AccessEntry(String, String),
3647 Idp(String, String),
3648 PodIdentity(String, String),
3649 Capability(String, String),
3650 Subscription(String),
3651 Generic(String),
3654}
3655
3656fn locate_tag_target(state: &crate::state::EksState, arn: &str) -> TagTarget {
3657 if let Some((n, _)) = state.clusters.iter().find(|(_, c)| c.arn == arn) {
3658 return TagTarget::Cluster(n.clone());
3659 }
3660 for (cn, m) in &state.nodegroups {
3661 if let Some((k, _)) = m.iter().find(|(_, r)| r.arn == arn) {
3662 return TagTarget::Nodegroup(cn.clone(), k.clone());
3663 }
3664 }
3665 for (cn, m) in &state.fargate_profiles {
3666 if let Some((k, _)) = m.iter().find(|(_, r)| r.arn == arn) {
3667 return TagTarget::Fargate(cn.clone(), k.clone());
3668 }
3669 }
3670 for (cn, m) in &state.addons {
3671 if let Some((k, _)) = m.iter().find(|(_, r)| r.arn == arn) {
3672 return TagTarget::Addon(cn.clone(), k.clone());
3673 }
3674 }
3675 for (cn, m) in &state.access_entries {
3676 if let Some((k, _)) = m.iter().find(|(_, r)| r.arn == arn) {
3677 return TagTarget::AccessEntry(cn.clone(), k.clone());
3678 }
3679 }
3680 for (cn, m) in &state.identity_provider_configs {
3681 if let Some((k, _)) = m.iter().find(|(_, r)| r.arn == arn) {
3682 return TagTarget::Idp(cn.clone(), k.clone());
3683 }
3684 }
3685 for (cn, m) in &state.pod_identity_associations {
3686 if let Some((k, _)) = m.iter().find(|(_, r)| r.association_arn == arn) {
3687 return TagTarget::PodIdentity(cn.clone(), k.clone());
3688 }
3689 }
3690 for (cn, m) in &state.capabilities {
3691 if let Some((k, _)) = m.iter().find(|(_, r)| r.arn == arn) {
3692 return TagTarget::Capability(cn.clone(), k.clone());
3693 }
3694 }
3695 if let Some((k, _)) = state
3696 .eks_anywhere_subscriptions
3697 .iter()
3698 .find(|(_, r)| r.arn == arn)
3699 {
3700 return TagTarget::Subscription(k.clone());
3701 }
3702 TagTarget::Generic(arn.to_string())
3703}
3704
3705fn tags_mut<'a>(
3706 state: &'a mut crate::state::EksState,
3707 target: &TagTarget,
3708) -> &'a mut crate::state::TagMap {
3709 match target {
3712 TagTarget::Cluster(n) => &mut state.clusters.get_mut(n).unwrap().tags,
3713 TagTarget::Nodegroup(c, k) => {
3714 &mut state
3715 .nodegroups
3716 .get_mut(c)
3717 .unwrap()
3718 .get_mut(k)
3719 .unwrap()
3720 .tags
3721 }
3722 TagTarget::Fargate(c, k) => {
3723 &mut state
3724 .fargate_profiles
3725 .get_mut(c)
3726 .unwrap()
3727 .get_mut(k)
3728 .unwrap()
3729 .tags
3730 }
3731 TagTarget::Addon(c, k) => &mut state.addons.get_mut(c).unwrap().get_mut(k).unwrap().tags,
3732 TagTarget::AccessEntry(c, k) => {
3733 &mut state
3734 .access_entries
3735 .get_mut(c)
3736 .unwrap()
3737 .get_mut(k)
3738 .unwrap()
3739 .tags
3740 }
3741 TagTarget::Idp(c, k) => {
3742 &mut state
3743 .identity_provider_configs
3744 .get_mut(c)
3745 .unwrap()
3746 .get_mut(k)
3747 .unwrap()
3748 .tags
3749 }
3750 TagTarget::PodIdentity(c, k) => {
3751 &mut state
3752 .pod_identity_associations
3753 .get_mut(c)
3754 .unwrap()
3755 .get_mut(k)
3756 .unwrap()
3757 .tags
3758 }
3759 TagTarget::Capability(c, k) => {
3760 &mut state
3761 .capabilities
3762 .get_mut(c)
3763 .unwrap()
3764 .get_mut(k)
3765 .unwrap()
3766 .tags
3767 }
3768 TagTarget::Subscription(k) => {
3769 &mut state.eks_anywhere_subscriptions.get_mut(k).unwrap().tags
3770 }
3771 TagTarget::Generic(a) => state.tags.entry(a.clone()).or_default(),
3772 }
3773}
3774
3775fn tags_ref<'a>(
3776 state: &'a crate::state::EksState,
3777 target: &TagTarget,
3778) -> Option<&'a crate::state::TagMap> {
3779 match target {
3780 TagTarget::Cluster(n) => state.clusters.get(n).map(|c| &c.tags),
3781 TagTarget::Nodegroup(c, k) => state
3782 .nodegroups
3783 .get(c)
3784 .and_then(|m| m.get(k))
3785 .map(|r| &r.tags),
3786 TagTarget::Fargate(c, k) => state
3787 .fargate_profiles
3788 .get(c)
3789 .and_then(|m| m.get(k))
3790 .map(|r| &r.tags),
3791 TagTarget::Addon(c, k) => state.addons.get(c).and_then(|m| m.get(k)).map(|r| &r.tags),
3792 TagTarget::AccessEntry(c, k) => state
3793 .access_entries
3794 .get(c)
3795 .and_then(|m| m.get(k))
3796 .map(|r| &r.tags),
3797 TagTarget::Idp(c, k) => state
3798 .identity_provider_configs
3799 .get(c)
3800 .and_then(|m| m.get(k))
3801 .map(|r| &r.tags),
3802 TagTarget::PodIdentity(c, k) => state
3803 .pod_identity_associations
3804 .get(c)
3805 .and_then(|m| m.get(k))
3806 .map(|r| &r.tags),
3807 TagTarget::Capability(c, k) => state
3808 .capabilities
3809 .get(c)
3810 .and_then(|m| m.get(k))
3811 .map(|r| &r.tags),
3812 TagTarget::Subscription(k) => state.eks_anywhere_subscriptions.get(k).map(|r| &r.tags),
3813 TagTarget::Generic(a) => state.tags.get(a),
3814 }
3815}
3816
3817fn parse_tag_map(v: Option<&Value>) -> crate::state::TagMap {
3818 let mut out = crate::state::TagMap::new();
3819 if let Some(obj) = v.and_then(|v| v.as_object()) {
3820 for (k, val) in obj {
3821 if let Some(s) = val.as_str() {
3822 out.insert(k.clone(), s.to_string());
3823 }
3824 }
3825 }
3826 out
3827}
3828
3829fn parse_multi_query(raw_query: &str, key: &str) -> Vec<String> {
3831 let prefix = format!("{key}=");
3832 raw_query
3833 .split('&')
3834 .filter(|pair| pair.starts_with(&prefix))
3835 .map(|pair| decode(&pair[prefix.len()..]))
3836 .collect()
3837}
3838
3839fn new_update(update_type: &str, params: Vec<(String, String)>) -> Update {
3840 Update {
3841 id: uuid::Uuid::new_v4().to_string(),
3842 status: "InProgress".to_string(),
3843 type_: update_type.to_string(),
3844 params,
3845 created_at: Utc::now(),
3846 }
3847}
3848
3849fn timestamp_to_number(t: DateTime<Utc>) -> Value {
3850 let secs = t.timestamp() as f64;
3851 let frac = t.timestamp_subsec_millis() as f64 / 1000.0;
3852 Value::from(secs + frac)
3853}
3854
3855fn arn_cluster_id(endpoint: &str) -> String {
3858 endpoint
3859 .strip_prefix("https://")
3860 .and_then(|s| s.split('.').next())
3861 .map(|s| s.to_lowercase())
3862 .unwrap_or_default()
3863}
3864
3865fn default_ca_data() -> String {
3866 "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCg==".to_string()
3868}
3869
3870fn build_vpc_config_response(req: &Value, id: &str) -> Value {
3871 let subnet_ids = req.get("subnetIds").cloned().unwrap_or_else(|| json!([]));
3872 let security_group_ids = req
3873 .get("securityGroupIds")
3874 .cloned()
3875 .unwrap_or_else(|| json!([]));
3876 let public_access_cidrs = req
3877 .get("publicAccessCidrs")
3878 .cloned()
3879 .unwrap_or_else(|| json!(["0.0.0.0/0"]));
3880 let endpoint_public = req
3881 .get("endpointPublicAccess")
3882 .and_then(|v| v.as_bool())
3883 .unwrap_or(true);
3884 let endpoint_private = req
3885 .get("endpointPrivateAccess")
3886 .and_then(|v| v.as_bool())
3887 .unwrap_or(false);
3888 let mut out = json!({
3889 "subnetIds": subnet_ids,
3890 "securityGroupIds": security_group_ids,
3891 "clusterSecurityGroupId": format!("sg-{}", &id.replace('-', "")[..17.min(id.replace('-', "").len())]),
3892 "vpcId": format!("vpc-{}", &id.replace('-', "")[..17.min(id.replace('-', "").len())]),
3893 "endpointPublicAccess": endpoint_public,
3894 "endpointPrivateAccess": endpoint_private,
3895 "publicAccessCidrs": public_access_cidrs,
3896 });
3897 if let Some(mode) = req.get("controlPlaneEgressMode") {
3898 out["controlPlaneEgressMode"] = mode.clone();
3899 }
3900 out
3901}
3902
3903fn build_k8s_network_config(req: Option<&Value>) -> Value {
3904 let ip_family = req
3905 .and_then(|v| v.get("ipFamily"))
3906 .and_then(|v| v.as_str())
3907 .unwrap_or("ipv4")
3908 .to_string();
3909 let service_cidr = req
3910 .and_then(|v| v.get("serviceIpv4Cidr"))
3911 .and_then(|v| v.as_str())
3912 .unwrap_or("172.20.0.0/16")
3913 .to_string();
3914 let elb_enabled = req
3917 .and_then(|v| v.get("elasticLoadBalancing"))
3918 .and_then(|v| v.get("enabled"))
3919 .and_then(|v| v.as_bool())
3920 .unwrap_or(false);
3921 json!({
3922 "serviceIpv4Cidr": service_cidr,
3923 "ipFamily": ip_family,
3924 "elasticLoadBalancing": { "enabled": elb_enabled },
3925 })
3926}
3927
3928fn build_access_config(req: Option<&Value>) -> Value {
3932 let mode = req
3933 .and_then(|v| v.get("authenticationMode"))
3934 .and_then(|v| v.as_str())
3935 .unwrap_or("CONFIG_MAP")
3936 .to_string();
3937 json!({ "authenticationMode": mode })
3938}
3939
3940fn build_upgrade_policy(req: Option<&Value>) -> Value {
3943 let support = req
3944 .and_then(|v| v.get("supportType"))
3945 .and_then(|v| v.as_str())
3946 .unwrap_or("EXTENDED")
3947 .to_string();
3948 json!({ "supportType": support })
3949}
3950
3951fn build_logging(req: Option<&Value>) -> Value {
3952 if let Some(setups) = req
3955 .and_then(|v| v.get("clusterLogging"))
3956 .and_then(|v| v.as_array())
3957 {
3958 return json!({ "clusterLogging": setups });
3959 }
3960 json!({
3961 "clusterLogging": [{
3962 "types": LOG_TYPES,
3963 "enabled": false,
3964 }],
3965 })
3966}
3967
3968fn cluster_json(c: &Cluster, id: &str) -> Value {
3969 let mut out = json!({
3970 "name": c.name,
3971 "arn": c.arn,
3972 "createdAt": timestamp_to_number(c.created_at),
3973 "version": c.version,
3974 "endpoint": c.endpoint,
3975 "roleArn": c.role_arn,
3976 "resourcesVpcConfig": c.resources_vpc_config,
3977 "kubernetesNetworkConfig": c.kubernetes_network_config,
3978 "logging": c.logging,
3979 "identity": {
3980 "oidc": {
3981 "issuer": format!("https://oidc.eks.amazonaws.com/id/{}", id.to_uppercase()),
3982 },
3983 },
3984 "status": c.status,
3985 "certificateAuthority": { "data": c.certificate_authority_data },
3986 "platformVersion": c.platform_version,
3987 "tags": c.tags,
3988 "health": { "issues": [] },
3989 "accessConfig": c.access_config,
3990 "upgradePolicy": c.upgrade_policy,
3991 });
3992 if let Some(cc) = &c.connector_config {
3993 out["connectorConfig"] = cc.clone();
3994 }
3995 if let Some(ec) = &c.encryption_config {
3996 out["encryptionConfig"] = ec.clone();
3997 }
3998 if let Some(v) = &c.compute_config {
3999 out["computeConfig"] = v.clone();
4000 }
4001 if let Some(v) = &c.storage_config {
4002 out["storageConfig"] = v.clone();
4003 }
4004 if let Some(v) = &c.zonal_shift_config {
4005 out["zonalShiftConfig"] = v.clone();
4006 }
4007 if let Some(v) = &c.remote_network_config {
4008 out["remoteNetworkConfig"] = v.clone();
4009 }
4010 if let Some(v) = &c.control_plane_scaling_config {
4011 out["controlPlaneScalingConfig"] = v.clone();
4012 }
4013 if let Some(v) = c.deletion_protection {
4014 out["deletionProtection"] = Value::Bool(v);
4015 }
4016 out
4017}
4018
4019fn update_json(u: &Update) -> Value {
4020 let params: Vec<Value> = u
4021 .params
4022 .iter()
4023 .map(|(t, v)| json!({ "type": t, "value": v }))
4024 .collect();
4025 json!({
4026 "id": u.id,
4027 "status": u.status,
4028 "type": u.type_,
4029 "params": params,
4030 "createdAt": timestamp_to_number(u.created_at),
4031 "errors": [],
4032 })
4033}
4034
4035fn build_scaling_config(req: Option<&Value>) -> Value {
4038 let min = req
4039 .and_then(|v| v.get("minSize"))
4040 .and_then(|v| v.as_i64())
4041 .unwrap_or(1);
4042 let max = req
4043 .and_then(|v| v.get("maxSize"))
4044 .and_then(|v| v.as_i64())
4045 .unwrap_or(2);
4046 let desired = req
4047 .and_then(|v| v.get("desiredSize"))
4048 .and_then(|v| v.as_i64())
4049 .unwrap_or(2);
4050 json!({ "minSize": min, "maxSize": max, "desiredSize": desired })
4051}
4052
4053fn build_nodegroup_update_config(req: Option<&Value>) -> Value {
4055 if let Some(pct) = req
4056 .and_then(|v| v.get("maxUnavailablePercentage"))
4057 .and_then(|v| v.as_i64())
4058 {
4059 return json!({ "maxUnavailablePercentage": pct });
4060 }
4061 let max_unavailable = req
4062 .and_then(|v| v.get("maxUnavailable"))
4063 .and_then(|v| v.as_i64())
4064 .unwrap_or(1);
4065 json!({ "maxUnavailable": max_unavailable })
4066}
4067
4068fn nodegroup_json(n: &Nodegroup) -> Value {
4069 let mut out = json!({
4070 "nodegroupName": n.name,
4071 "nodegroupArn": n.arn,
4072 "clusterName": n.cluster_name,
4073 "version": n.version,
4074 "releaseVersion": n.release_version,
4075 "createdAt": timestamp_to_number(n.created_at),
4076 "modifiedAt": timestamp_to_number(n.modified_at),
4077 "status": n.status,
4078 "capacityType": n.capacity_type,
4079 "scalingConfig": n.scaling_config,
4080 "instanceTypes": n.instance_types,
4081 "subnets": n.subnets,
4082 "amiType": n.ami_type,
4083 "nodeRole": n.node_role,
4084 "labels": n.labels,
4085 "taints": n.taints,
4086 "resources": {
4087 "autoScalingGroups": [{ "name": n.asg_name }],
4088 },
4089 "diskSize": n.disk_size,
4090 "health": { "issues": [] },
4091 "updateConfig": n.update_config,
4092 "tags": n.tags,
4093 });
4094 if let Some(ra) = &n.remote_access {
4095 out["remoteAccess"] = ra.clone();
4096 }
4097 if let Some(lt) = &n.launch_template {
4098 out["launchTemplate"] = lt.clone();
4099 }
4100 out
4101}
4102
4103fn fargate_profile_json(p: &FargateProfile) -> Value {
4104 json!({
4105 "fargateProfileName": p.name,
4106 "fargateProfileArn": p.arn,
4107 "clusterName": p.cluster_name,
4108 "createdAt": timestamp_to_number(p.created_at),
4109 "podExecutionRoleArn": p.pod_execution_role_arn,
4110 "subnets": p.subnets,
4111 "selectors": p.selectors,
4112 "status": p.status,
4113 "tags": p.tags,
4114 "health": { "issues": [] },
4115 })
4116}
4117
4118fn default_addon_version(addon_name: &str, _cluster_version: &str) -> String {
4122 match addon_name {
4123 "vpc-cni" => "v1.18.3-eksbuild.2".to_string(),
4124 "coredns" => "v1.11.1-eksbuild.9".to_string(),
4125 "kube-proxy" => "v1.31.0-eksbuild.2".to_string(),
4126 "aws-ebs-csi-driver" => "v1.35.0-eksbuild.1".to_string(),
4127 "aws-efs-csi-driver" => "v2.1.0-eksbuild.1".to_string(),
4128 _ => "v1.0.0-eksbuild.1".to_string(),
4129 }
4130}
4131
4132fn build_pod_identity_association_arns(
4136 region: &str,
4137 account_id: &str,
4138 cluster: &str,
4139 req: Option<&Value>,
4140) -> Vec<String> {
4141 let Some(list) = req.and_then(|v| v.as_array()) else {
4142 return Vec::new();
4143 };
4144 list.iter()
4145 .map(|_| {
4146 let id = uuid::Uuid::new_v4().to_string().replace('-', "");
4147 pod_identity_association_arn(region, account_id, cluster, &id[..17.min(id.len())])
4148 })
4149 .collect()
4150}
4151
4152fn addon_json(a: &Addon) -> Value {
4153 let mut out = json!({
4154 "addonName": a.name,
4155 "clusterName": a.cluster_name,
4156 "status": a.status,
4157 "addonVersion": a.addon_version,
4158 "addonArn": a.arn,
4159 "createdAt": timestamp_to_number(a.created_at),
4160 "modifiedAt": timestamp_to_number(a.modified_at),
4161 "tags": a.tags,
4162 "health": { "issues": [] },
4163 });
4164 if let Some(role) = &a.service_account_role_arn {
4165 out["serviceAccountRoleArn"] = Value::String(role.clone());
4166 }
4167 if let Some(cfg) = &a.configuration_values {
4168 out["configurationValues"] = Value::String(cfg.clone());
4169 }
4170 if let Some(ns) = &a.namespace {
4171 out["namespaceConfig"] = json!({ "namespace": ns });
4172 }
4173 if !a.pod_identity_associations.is_empty() {
4174 out["podIdentityAssociations"] = json!(a.pod_identity_associations);
4175 }
4176 out
4177}
4178
4179fn addon_version_info(version: &str, cluster_version: &str, requires_config: bool) -> Value {
4181 json!({
4182 "addonVersion": version,
4183 "architecture": ["amd64", "arm64"],
4184 "computeTypes": ["ec2", "fargate"],
4185 "compatibilities": [{
4186 "clusterVersion": cluster_version,
4187 "platformVersions": ["*"],
4188 "defaultVersion": true,
4189 }],
4190 "requiresConfiguration": requires_config,
4191 "requiresIamPermissions": false,
4192 })
4193}
4194
4195fn addon_catalog(cluster_version: &str) -> Vec<Value> {
4198 let entry = |name: &str, atype: &str, ns: &str, requires_config: bool| -> Value {
4199 let versions = vec![addon_version_info(
4200 &default_addon_version(name, cluster_version),
4201 cluster_version,
4202 requires_config,
4203 )];
4204 json!({
4205 "addonName": name,
4206 "type": atype,
4207 "addonVersions": versions,
4208 "publisher": "eks",
4209 "owner": "aws",
4210 "defaultNamespace": ns,
4211 })
4212 };
4213 vec![
4214 entry("vpc-cni", "networking", "kube-system", false),
4215 entry("coredns", "networking", "kube-system", false),
4216 entry("kube-proxy", "networking", "kube-system", false),
4217 entry("aws-ebs-csi-driver", "storage", "kube-system", false),
4218 entry("aws-efs-csi-driver", "storage", "kube-system", false),
4219 ]
4220}
4221
4222fn addon_configuration_schema(addon_name: &str) -> String {
4225 json!({
4226 "$schema": "http://json-schema.org/draft-07/schema#",
4227 "type": "object",
4228 "title": format!("{addon_name} configuration schema"),
4229 "additionalProperties": false,
4230 "properties": {
4231 "resources": {
4232 "type": "object",
4233 "additionalProperties": false,
4234 "properties": {
4235 "limits": { "type": "object" },
4236 "requests": { "type": "object" },
4237 },
4238 },
4239 "tolerations": { "type": "array" },
4240 "nodeSelector": { "type": "object" },
4241 },
4242 })
4243 .to_string()
4244}
4245
4246fn pod_identity_configuration(addon_name: &str) -> Value {
4249 match addon_name {
4250 "vpc-cni" => json!([{
4251 "serviceAccount": "aws-node",
4252 "recommendedManagedPolicies": ["arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"],
4253 }]),
4254 "aws-ebs-csi-driver" => json!([{
4255 "serviceAccount": "ebs-csi-controller-sa",
4256 "recommendedManagedPolicies": ["arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"],
4257 }]),
4258 "aws-efs-csi-driver" => json!([{
4259 "serviceAccount": "efs-csi-controller-sa",
4260 "recommendedManagedPolicies": ["arn:aws:iam::aws:policy/service-role/AmazonEFSCSIDriverPolicy"],
4261 }]),
4262 _ => json!([]),
4263 }
4264}
4265
4266fn principal_parts(principal_arn: &str) -> (String, String) {
4270 let resource = principal_arn
4271 .split(':')
4272 .next_back()
4273 .unwrap_or(principal_arn);
4274 let (kind, name) = resource.split_once('/').unwrap_or(("", resource));
4275 let name = name.rsplit('/').next().unwrap_or(name);
4276 let type_seg = match kind {
4277 "role" | "assumed-role" => "role",
4278 "user" => "user",
4279 _ => "standard",
4280 };
4281 (type_seg.to_string(), name.to_string())
4282}
4283
4284fn default_username(principal_arn: &str) -> String {
4288 let (type_seg, name) = principal_parts(principal_arn);
4289 match type_seg.as_str() {
4290 "role" => format!("arn:aws:sts::{{{{AccountID}}}}:assumed-role/{name}/{{{{SessionName}}}}"),
4291 _ => principal_arn.to_string(),
4292 }
4293}
4294
4295fn string_list(v: Option<&Value>) -> Vec<String> {
4297 v.and_then(|v| v.as_array())
4298 .map(|arr| {
4299 arr.iter()
4300 .filter_map(|x| x.as_str().map(|s| s.to_string()))
4301 .collect()
4302 })
4303 .unwrap_or_default()
4304}
4305
4306fn build_access_scope(req: Option<&Value>) -> Value {
4309 let scope_type = req
4310 .and_then(|v| v.get("type"))
4311 .and_then(|v| v.as_str())
4312 .unwrap_or("cluster")
4313 .to_string();
4314 let namespaces = req
4315 .and_then(|v| v.get("namespaces"))
4316 .cloned()
4317 .unwrap_or_else(|| json!([]));
4318 json!({ "type": scope_type, "namespaces": namespaces })
4319}
4320
4321fn access_entry_json(e: &AccessEntry) -> Value {
4322 json!({
4323 "clusterName": e.cluster_name,
4324 "principalArn": e.principal_arn,
4325 "kubernetesGroups": e.kubernetes_groups,
4326 "accessEntryArn": e.arn,
4327 "createdAt": timestamp_to_number(e.created_at),
4328 "modifiedAt": timestamp_to_number(e.modified_at),
4329 "tags": e.tags,
4330 "username": e.username,
4331 "type": e.type_,
4332 })
4333}
4334
4335fn associated_policy_json(ap: &AssociatedPolicy) -> Value {
4336 json!({
4337 "policyArn": ap.policy_arn,
4338 "accessScope": ap.access_scope,
4339 "associatedAt": timestamp_to_number(ap.associated_at),
4340 "modifiedAt": timestamp_to_number(ap.modified_at),
4341 })
4342}
4343
4344fn str_field(v: &Value, key: &str) -> Option<String> {
4346 v.get(key).and_then(|x| x.as_str()).map(|s| s.to_string())
4347}
4348
4349fn identity_provider_config_json(c: &IdentityProviderConfig) -> Value {
4350 let mut oidc = json!({
4351 "identityProviderConfigName": c.name,
4352 "identityProviderConfigArn": c.arn,
4353 "clusterName": c.cluster_name,
4354 "issuerUrl": c.issuer_url,
4355 "clientId": c.client_id,
4356 "requiredClaims": c.required_claims,
4357 "status": c.status,
4358 "tags": c.tags,
4359 });
4360 if let Some(v) = &c.username_claim {
4361 oidc["usernameClaim"] = Value::String(v.clone());
4362 }
4363 if let Some(v) = &c.username_prefix {
4364 oidc["usernamePrefix"] = Value::String(v.clone());
4365 }
4366 if let Some(v) = &c.groups_claim {
4367 oidc["groupsClaim"] = Value::String(v.clone());
4368 }
4369 if let Some(v) = &c.groups_prefix {
4370 oidc["groupsPrefix"] = Value::String(v.clone());
4371 }
4372 json!({ "oidc": oidc })
4373}
4374
4375fn pod_identity_association_json(a: &PodIdentityAssociation) -> Value {
4376 let mut out = json!({
4377 "clusterName": a.cluster_name,
4378 "namespace": a.namespace,
4379 "serviceAccount": a.service_account,
4380 "roleArn": a.role_arn,
4381 "associationArn": a.association_arn,
4382 "associationId": a.association_id,
4383 "createdAt": timestamp_to_number(a.created_at),
4384 "modifiedAt": timestamp_to_number(a.modified_at),
4385 "disableSessionTags": a.disable_session_tags,
4386 "tags": a.tags,
4387 });
4388 if let Some(v) = &a.target_role_arn {
4389 out["targetRoleArn"] = Value::String(v.clone());
4390 }
4391 if let Some(v) = &a.external_id {
4392 out["externalId"] = Value::String(v.clone());
4393 }
4394 out
4395}
4396
4397fn pod_identity_association_summary_json(a: &PodIdentityAssociation) -> Value {
4398 json!({
4399 "clusterName": a.cluster_name,
4400 "namespace": a.namespace,
4401 "serviceAccount": a.service_account,
4402 "associationArn": a.association_arn,
4403 "associationId": a.association_id,
4404 })
4405}
4406
4407fn access_policy_catalog() -> Vec<Value> {
4411 const POLICIES: &[&str] = &[
4412 "AmazonEKSClusterAdminPolicy",
4413 "AmazonEKSAdminPolicy",
4414 "AmazonEKSEditPolicy",
4415 "AmazonEKSViewPolicy",
4416 "AmazonEKSAdminViewPolicy",
4417 "AmazonEKSAutoNodePolicy",
4418 "AmazonEKSBlockStoragePolicy",
4419 "AmazonEKSLoadBalancingPolicy",
4420 "AmazonEKSNetworkingPolicy",
4421 "AmazonEKSComputePolicy",
4422 ];
4423 POLICIES
4424 .iter()
4425 .map(|name| {
4426 json!({
4427 "name": name,
4428 "arn": format!("arn:aws:eks::aws:cluster-access-policy/{name}"),
4429 })
4430 })
4431 .collect()
4432}
4433
4434fn not_found_insight(id: &str) -> impl Fn() -> AwsServiceError + 'static {
4435 let id = id.to_string();
4436 move || {
4437 AwsServiceError::aws_error(
4438 StatusCode::NOT_FOUND,
4439 "ResourceNotFoundException",
4440 format!("No insight found for id: {id}."),
4441 )
4442 }
4443}
4444
4445fn not_found_capability(name: &str) -> impl Fn() -> AwsServiceError + 'static {
4446 let name = name.to_string();
4447 move || {
4448 AwsServiceError::aws_error(
4449 StatusCode::NOT_FOUND,
4450 "ResourceNotFoundException",
4451 format!("No capability found for name: {name}."),
4452 )
4453 }
4454}
4455
4456fn not_found_subscription(id: &str) -> impl Fn() -> AwsServiceError + 'static {
4457 let id = id.to_string();
4458 move || {
4459 AwsServiceError::aws_error(
4460 StatusCode::NOT_FOUND,
4461 "ResourceNotFoundException",
4462 format!("No EKS Anywhere subscription found for id: {id}."),
4463 )
4464 }
4465}
4466
4467fn connected_cluster_json(c: &Cluster, id: &str) -> Value {
4470 cluster_json(c, id)
4471}
4472
4473fn default_insights(cluster_version: &str) -> Vec<Insight> {
4477 let now = Utc::now();
4478 let mk = |name: &str, category: &str, description: &str, recommendation: &str| Insight {
4479 id: uuid::Uuid::new_v4().to_string(),
4480 name: name.to_string(),
4481 category: category.to_string(),
4482 kubernetes_version: cluster_version.to_string(),
4483 description: description.to_string(),
4484 recommendation: recommendation.to_string(),
4485 status: "PASSING".to_string(),
4486 reason: "No deprecated API usage detected.".to_string(),
4487 last_refresh_time: now,
4488 last_transition_time: now,
4489 };
4490 vec![
4491 mk(
4492 "Deprecated APIs removed in Kubernetes",
4493 "UPGRADE_READINESS",
4494 "Checks for usage of Kubernetes APIs that are removed in the next version.",
4495 "Migrate any deprecated API usage to the supported apiVersion before upgrading.",
4496 ),
4497 mk(
4498 "Kubelet version skew",
4499 "UPGRADE_READINESS",
4500 "Checks that node kubelet versions are within the supported skew of the control plane.",
4501 "Upgrade node groups so kubelet stays within two minor versions of the control plane.",
4502 ),
4503 mk(
4504 "EKS add-on version compatibility",
4505 "UPGRADE_READINESS",
4506 "Checks that installed EKS add-on versions are compatible with the target cluster version.",
4507 "Update add-ons to a version compatible with the target Kubernetes version.",
4508 ),
4509 ]
4510}
4511
4512fn insight_status_json(i: &Insight) -> Value {
4513 json!({ "status": i.status, "reason": i.reason })
4514}
4515
4516fn insight_json(i: &Insight) -> Value {
4517 json!({
4518 "id": i.id,
4519 "name": i.name,
4520 "category": i.category,
4521 "kubernetesVersion": i.kubernetes_version,
4522 "lastRefreshTime": timestamp_to_number(i.last_refresh_time),
4523 "lastTransitionTime": timestamp_to_number(i.last_transition_time),
4524 "description": i.description,
4525 "insightStatus": insight_status_json(i),
4526 "recommendation": i.recommendation,
4527 "additionalInfo": {},
4528 "resources": [],
4529 })
4530}
4531
4532fn insight_summary_json(i: &Insight) -> Value {
4533 json!({
4534 "id": i.id,
4535 "name": i.name,
4536 "category": i.category,
4537 "kubernetesVersion": i.kubernetes_version,
4538 "lastRefreshTime": timestamp_to_number(i.last_refresh_time),
4539 "lastTransitionTime": timestamp_to_number(i.last_transition_time),
4540 "description": i.description,
4541 "insightStatus": insight_status_json(i),
4542 })
4543}
4544
4545fn insights_refresh_json(r: &InsightsRefresh) -> Value {
4546 let mut out = json!({
4547 "message": "Insights refresh for the cluster.",
4548 "status": r.status,
4549 "startedAt": timestamp_to_number(r.started_at),
4550 });
4551 if let Some(ended) = r.ended_at {
4552 out["endedAt"] = timestamp_to_number(ended);
4553 }
4554 out
4555}
4556
4557fn cluster_version_catalog(cluster_type: &str) -> Vec<Value> {
4561 type VersionRow = (
4564 &'static str,
4565 &'static str,
4566 &'static str,
4567 &'static str,
4568 &'static str,
4569 &'static str,
4570 &'static str,
4571 bool,
4572 );
4573 const ROWS: &[VersionRow] = &[
4574 (
4575 "1.28",
4576 "1.28.15",
4577 "eks.30",
4578 "2023-09-26",
4579 "2024-11-26",
4580 "2025-11-26",
4581 "extended_support",
4582 false,
4583 ),
4584 (
4585 "1.29",
4586 "1.29.10",
4587 "eks.24",
4588 "2024-01-23",
4589 "2025-03-23",
4590 "2026-03-23",
4591 "extended_support",
4592 false,
4593 ),
4594 (
4595 "1.30",
4596 "1.30.6",
4597 "eks.20",
4598 "2024-05-23",
4599 "2025-07-23",
4600 "2026-07-23",
4601 "standard_support",
4602 false,
4603 ),
4604 (
4605 "1.31",
4606 "1.31.2",
4607 "eks.9",
4608 "2024-09-26",
4609 "2025-11-26",
4610 "2026-11-26",
4611 "standard_support",
4612 true,
4613 ),
4614 (
4615 "1.32",
4616 "1.32.0",
4617 "eks.2",
4618 "2025-01-23",
4619 "2026-03-23",
4620 "2027-03-23",
4621 "standard_support",
4622 false,
4623 ),
4624 ];
4625 ROWS.iter()
4626 .map(
4627 |(ver, patch, plat, release, eos, eoe, status, is_default)| {
4628 let version_status = match *status {
4629 "standard_support" => "STANDARD_SUPPORT",
4630 "extended_support" => "EXTENDED_SUPPORT",
4631 _ => "UNSUPPORTED",
4632 };
4633 json!({
4634 "clusterVersion": ver,
4635 "clusterType": cluster_type,
4636 "defaultPlatformVersion": plat,
4637 "defaultVersion": is_default,
4638 "releaseDate": date_to_number(release),
4639 "endOfStandardSupportDate": date_to_number(eos),
4640 "endOfExtendedSupportDate": date_to_number(eoe),
4641 "status": status,
4642 "versionStatus": version_status,
4643 "kubernetesPatchVersion": patch,
4644 })
4645 },
4646 )
4647 .collect()
4648}
4649
4650fn date_to_number(ymd: &str) -> Value {
4652 let ts = chrono::NaiveDate::parse_from_str(ymd, "%Y-%m-%d")
4653 .ok()
4654 .and_then(|d| d.and_hms_opt(0, 0, 0))
4655 .map(|dt| dt.and_utc())
4656 .unwrap_or_else(Utc::now);
4657 timestamp_to_number(ts)
4658}
4659
4660fn normalize_capability_configuration(req: Option<&Value>) -> Option<Value> {
4664 let argo = req.and_then(|v| v.get("argoCd"))?;
4665 Some(json!({ "argoCd": argo }))
4666}
4667
4668fn capability_json(c: &Capability) -> Value {
4669 let mut out = json!({
4670 "capabilityName": c.name,
4671 "arn": c.arn,
4672 "clusterName": c.cluster_name,
4673 "type": c.type_,
4674 "roleArn": c.role_arn,
4675 "status": c.status,
4676 "version": c.version,
4677 "tags": c.tags,
4678 "health": { "issues": [] },
4679 "createdAt": timestamp_to_number(c.created_at),
4680 "modifiedAt": timestamp_to_number(c.modified_at),
4681 });
4682 if let Some(cfg) = &c.configuration {
4683 out["configuration"] = cfg.clone();
4684 }
4685 if let Some(p) = &c.delete_propagation_policy {
4686 out["deletePropagationPolicy"] = Value::String(p.clone());
4687 }
4688 out
4689}
4690
4691fn capability_summary_json(c: &Capability) -> Value {
4692 json!({
4693 "capabilityName": c.name,
4694 "arn": c.arn,
4695 "type": c.type_,
4696 "status": c.status,
4697 "version": c.version,
4698 "createdAt": timestamp_to_number(c.created_at),
4699 "modifiedAt": timestamp_to_number(c.modified_at),
4700 })
4701}
4702
4703fn subscription_json(s: &EksAnywhereSubscription) -> Value {
4704 json!({
4705 "id": s.id,
4706 "arn": s.arn,
4707 "createdAt": timestamp_to_number(s.created_at),
4708 "effectiveDate": timestamp_to_number(s.effective_date),
4709 "expirationDate": timestamp_to_number(s.expiration_date),
4710 "licenseQuantity": s.license_quantity,
4711 "licenseType": s.license_type,
4712 "term": { "duration": s.term_duration, "unit": s.term_unit },
4713 "status": s.status,
4714 "autoRenew": s.auto_renew,
4715 "licenseArns": [],
4716 "licenses": [],
4717 "tags": s.tags,
4718 })
4719}
4720
4721#[cfg(test)]
4722mod tests {
4723 use super::*;
4724 use bytes::Bytes;
4725 use http::HeaderMap;
4726 use parking_lot::RwLock;
4727 use std::collections::HashMap;
4728
4729 fn make_state() -> SharedEksState {
4730 Arc::new(RwLock::new(
4731 fakecloud_core::multi_account::MultiAccountState::new("111122223333", "us-east-1", ""),
4732 ))
4733 }
4734
4735 fn make_request(method: Method, path: &str, body: &str) -> AwsRequest {
4736 let (p, q) = match path.find('?') {
4737 Some(i) => (&path[..i], &path[i + 1..]),
4738 None => (path, ""),
4739 };
4740 let path_segments: Vec<String> = p
4741 .split('/')
4742 .filter(|s| !s.is_empty())
4743 .map(|s| s.to_string())
4744 .collect();
4745 let query_params: HashMap<String, String> = q
4746 .split('&')
4747 .filter(|s| !s.is_empty())
4748 .filter_map(|pair| {
4749 let (k, v) = pair.split_once('=')?;
4750 Some((k.to_string(), v.to_string()))
4751 })
4752 .collect();
4753 AwsRequest {
4754 service: "eks".to_string(),
4755 action: String::new(),
4756 region: "us-east-1".to_string(),
4757 account_id: "111122223333".to_string(),
4758 request_id: "test".to_string(),
4759 headers: HeaderMap::new(),
4760 query_params,
4761 body: Bytes::from(body.to_string()),
4762 body_stream: parking_lot::Mutex::new(None),
4763 path_segments,
4764 raw_path: p.to_string(),
4765 raw_query: q.to_string(),
4766 method,
4767 is_query_protocol: false,
4768 access_key_id: None,
4769 principal: None,
4770 }
4771 }
4772
4773 fn create_body(name: &str) -> String {
4774 json!({
4775 "name": name,
4776 "roleArn": "arn:aws:iam::111122223333:role/eks-cluster",
4777 "resourcesVpcConfig": {
4778 "subnetIds": ["subnet-1", "subnet-2"],
4779 "endpointPublicAccess": true
4780 }
4781 })
4782 .to_string()
4783 }
4784
4785 #[test]
4786 fn snapshot_hook_is_none_without_store() {
4787 let svc = EksService::new(make_state());
4788 assert!(svc.snapshot_hook().is_none());
4789 }
4790
4791 #[tokio::test]
4792 async fn snapshot_hook_fires_with_store() {
4793 let store: Arc<dyn SnapshotStore> =
4794 Arc::new(fakecloud_persistence::MemorySnapshotStore::new());
4795 let svc = EksService::new(make_state()).with_snapshot_store(store);
4796 let hook = svc.snapshot_hook().expect("hook present when store set");
4797 hook().await;
4798 }
4799
4800 #[tokio::test]
4801 async fn create_describe_round_trip_settles_active() {
4802 let svc = EksService::new(make_state());
4803 let resp = svc
4804 .handle(make_request(
4805 Method::POST,
4806 "/clusters",
4807 &create_body("demo"),
4808 ))
4809 .await
4810 .unwrap();
4811 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4812 assert_eq!(v["cluster"]["name"], "demo");
4813 assert_eq!(v["cluster"]["status"], "CREATING");
4814 assert_eq!(
4815 v["cluster"]["arn"],
4816 "arn:aws:eks:us-east-1:111122223333:cluster/demo"
4817 );
4818 assert_eq!(v["cluster"]["version"], DEFAULT_K8S_VERSION);
4819
4820 let resp = svc
4821 .handle(make_request(Method::GET, "/clusters/demo", ""))
4822 .await
4823 .unwrap();
4824 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4825 assert_eq!(v["cluster"]["status"], "ACTIVE");
4826 assert_eq!(
4827 v["cluster"]["resourcesVpcConfig"]["endpointPublicAccess"],
4828 true
4829 );
4830 }
4831
4832 #[tokio::test]
4833 async fn create_rejects_duplicate() {
4834 let svc = EksService::new(make_state());
4835 svc.handle(make_request(Method::POST, "/clusters", &create_body("dup")))
4836 .await
4837 .unwrap();
4838 let err = svc
4839 .handle(make_request(Method::POST, "/clusters", &create_body("dup")))
4840 .await
4841 .err()
4842 .unwrap();
4843 assert_eq!(err.status(), StatusCode::CONFLICT);
4844 assert_eq!(err.code(), "ResourceInUseException");
4845 }
4846
4847 #[tokio::test]
4848 async fn describe_missing_cluster_is_not_found() {
4849 let svc = EksService::new(make_state());
4850 let err = svc
4851 .handle(make_request(Method::GET, "/clusters/ghost", ""))
4852 .await
4853 .err()
4854 .unwrap();
4855 assert_eq!(err.status(), StatusCode::NOT_FOUND);
4856 assert_eq!(err.code(), "ResourceNotFoundException");
4857 }
4858
4859 #[tokio::test]
4860 async fn list_then_delete_cluster() {
4861 let svc = EksService::new(make_state());
4862 svc.handle(make_request(Method::POST, "/clusters", &create_body("c1")))
4863 .await
4864 .unwrap();
4865 let resp = svc
4866 .handle(make_request(Method::GET, "/clusters", ""))
4867 .await
4868 .unwrap();
4869 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4870 assert_eq!(v["clusters"], json!(["c1"]));
4871
4872 let resp = svc
4873 .handle(make_request(Method::DELETE, "/clusters/c1", ""))
4874 .await
4875 .unwrap();
4876 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4877 assert_eq!(v["cluster"]["status"], "DELETING");
4878
4879 let err = svc
4880 .handle(make_request(Method::GET, "/clusters/c1", ""))
4881 .await
4882 .err()
4883 .unwrap();
4884 assert_eq!(err.status(), StatusCode::NOT_FOUND);
4885 }
4886
4887 #[tokio::test]
4888 async fn cluster_reports_access_config_upgrade_policy_and_elb_defaults() {
4889 let svc = EksService::new(make_state());
4890 let resp = svc
4893 .handle(make_request(Method::POST, "/clusters", &create_body("c1")))
4894 .await
4895 .unwrap();
4896 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4897 let c = &v["cluster"];
4898 assert_eq!(c["accessConfig"]["authenticationMode"], "CONFIG_MAP");
4899 assert_eq!(c["upgradePolicy"]["supportType"], "EXTENDED");
4900 assert_eq!(
4901 c["kubernetesNetworkConfig"]["elasticLoadBalancing"]["enabled"],
4902 false
4903 );
4904 assert_eq!(
4905 c["kubernetesNetworkConfig"]["serviceIpv4Cidr"],
4906 "172.20.0.0/16"
4907 );
4908 assert_eq!(c["kubernetesNetworkConfig"]["ipFamily"], "ipv4");
4909
4910 let resp = svc
4912 .handle(make_request(Method::GET, "/clusters/c1", ""))
4913 .await
4914 .unwrap();
4915 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4916 assert_eq!(
4917 v["cluster"]["accessConfig"]["authenticationMode"],
4918 "CONFIG_MAP"
4919 );
4920 assert_eq!(v["cluster"]["upgradePolicy"]["supportType"], "EXTENDED");
4921 assert_eq!(
4922 v["cluster"]["kubernetesNetworkConfig"]["elasticLoadBalancing"]["enabled"],
4923 false
4924 );
4925 }
4926
4927 #[tokio::test]
4928 async fn cluster_echoes_supplied_access_config() {
4929 let svc = EksService::new(make_state());
4930 let body = json!({
4931 "name": "c1",
4932 "roleArn": "arn:aws:iam::111122223333:role/eks-cluster",
4933 "resourcesVpcConfig": { "subnetIds": ["subnet-1", "subnet-2"] },
4934 "accessConfig": { "authenticationMode": "API" },
4935 })
4936 .to_string();
4937 let resp = svc
4938 .handle(make_request(Method::POST, "/clusters", &body))
4939 .await
4940 .unwrap();
4941 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4942 assert_eq!(v["cluster"]["accessConfig"]["authenticationMode"], "API");
4943 }
4944
4945 #[tokio::test]
4946 async fn update_version_and_describe_update() {
4947 let svc = EksService::new(make_state());
4948 svc.handle(make_request(Method::POST, "/clusters", &create_body("up")))
4949 .await
4950 .unwrap();
4951 let resp = svc
4952 .handle(make_request(
4953 Method::POST,
4954 "/clusters/up/updates",
4955 &json!({ "version": "1.32" }).to_string(),
4956 ))
4957 .await
4958 .unwrap();
4959 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4960 assert_eq!(v["update"]["type"], "VersionUpdate");
4961 assert_eq!(v["update"]["status"], "InProgress");
4962 let update_id = v["update"]["id"].as_str().unwrap().to_string();
4963
4964 let resp = svc
4965 .handle(make_request(
4966 Method::GET,
4967 &format!("/clusters/up/updates/{update_id}"),
4968 "",
4969 ))
4970 .await
4971 .unwrap();
4972 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4973 assert_eq!(v["update"]["status"], "Successful");
4974
4975 let resp = svc
4977 .handle(make_request(Method::GET, "/clusters/up", ""))
4978 .await
4979 .unwrap();
4980 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4981 assert_eq!(v["cluster"]["version"], "1.32");
4982
4983 let resp = svc
4984 .handle(make_request(Method::GET, "/clusters/up/updates", ""))
4985 .await
4986 .unwrap();
4987 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
4988 assert_eq!(v["updateIds"].as_array().unwrap().len(), 1);
4989 }
4990
4991 #[tokio::test]
4992 async fn tag_untag_round_trip() {
4993 let svc = EksService::new(make_state());
4994 svc.handle(make_request(Method::POST, "/clusters", &create_body("tg")))
4995 .await
4996 .unwrap();
4997 let arn = "arn:aws:eks:us-east-1:111122223333:cluster/tg";
4998 let encoded = arn.replace('/', "%2F");
4999 svc.handle(make_request(
5000 Method::POST,
5001 &format!("/tags/{encoded}"),
5002 &json!({ "tags": { "env": "prod" } }).to_string(),
5003 ))
5004 .await
5005 .unwrap();
5006
5007 let resp = svc
5008 .handle(make_request(Method::GET, &format!("/tags/{encoded}"), ""))
5009 .await
5010 .unwrap();
5011 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5012 assert_eq!(v["tags"]["env"], "prod");
5013
5014 svc.handle(make_request(
5015 Method::DELETE,
5016 &format!("/tags/{encoded}?tagKeys=env"),
5017 "",
5018 ))
5019 .await
5020 .unwrap();
5021 let resp = svc
5022 .handle(make_request(Method::GET, &format!("/tags/{encoded}"), ""))
5023 .await
5024 .unwrap();
5025 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5026 assert_eq!(v["tags"], json!({}));
5027 }
5028
5029 #[tokio::test]
5030 async fn update_cluster_config_persists_access_and_upgrade() {
5031 let svc = EksService::new(make_state());
5032 svc.handle(make_request(Method::POST, "/clusters", &create_body("uc")))
5033 .await
5034 .unwrap();
5035 svc.handle(make_request(Method::GET, "/clusters/uc", ""))
5037 .await
5038 .unwrap();
5039 let resp = svc
5040 .handle(make_request(
5041 Method::POST,
5042 "/clusters/uc/update-config",
5043 &json!({
5044 "accessConfig": { "authenticationMode": "API_AND_CONFIG_MAP" },
5045 "upgradePolicy": { "supportType": "STANDARD" },
5046 "computeConfig": { "enabled": true }
5047 })
5048 .to_string(),
5049 ))
5050 .await
5051 .unwrap();
5052 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5053 assert_eq!(v["update"]["status"], "InProgress");
5054
5055 let resp = svc
5056 .handle(make_request(Method::GET, "/clusters/uc", ""))
5057 .await
5058 .unwrap();
5059 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5060 assert_eq!(
5061 v["cluster"]["accessConfig"]["authenticationMode"],
5062 "API_AND_CONFIG_MAP"
5063 );
5064 assert_eq!(v["cluster"]["upgradePolicy"]["supportType"], "STANDARD");
5065 assert_eq!(v["cluster"]["computeConfig"]["enabled"], true);
5066 }
5067
5068 #[tokio::test]
5069 async fn tag_nodegroup_sub_resource_arn() {
5070 let svc = EksService::new(make_state());
5071 create_cluster(&svc, "tc").await;
5072 let resp = svc
5073 .handle(make_request(
5074 Method::POST,
5075 "/clusters/tc/node-groups",
5076 &nodegroup_body("tng"),
5077 ))
5078 .await
5079 .unwrap();
5080 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5081 let ng_arn = v["nodegroup"]["nodegroupArn"].as_str().unwrap().to_string();
5082 let encoded = ng_arn.replace('/', "%2F").replace(':', "%3A");
5083
5084 svc.handle(make_request(
5086 Method::POST,
5087 &format!("/tags/{encoded}"),
5088 &json!({ "tags": { "team": "core" } }).to_string(),
5089 ))
5090 .await
5091 .unwrap();
5092
5093 let resp = svc
5094 .handle(make_request(Method::GET, &format!("/tags/{encoded}"), ""))
5095 .await
5096 .unwrap();
5097 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5098 assert_eq!(v["tags"]["team"], "core");
5099
5100 let resp = svc
5102 .handle(make_request(
5103 Method::GET,
5104 "/clusters/tc/node-groups/tng",
5105 "",
5106 ))
5107 .await
5108 .unwrap();
5109 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5110 assert_eq!(v["nodegroup"]["tags"]["team"], "core");
5111 }
5112
5113 #[tokio::test]
5114 async fn delete_cluster_blocked_by_live_nodegroup() {
5115 let svc = EksService::new(make_state());
5116 create_cluster(&svc, "bc").await;
5117 svc.handle(make_request(
5118 Method::POST,
5119 "/clusters/bc/node-groups",
5120 &nodegroup_body("ng"),
5121 ))
5122 .await
5123 .unwrap();
5124 let err = svc
5125 .handle(make_request(Method::DELETE, "/clusters/bc", ""))
5126 .await
5127 .err()
5128 .unwrap();
5129 assert_eq!(err.status(), StatusCode::CONFLICT);
5130 assert_eq!(err.code(), "ResourceInUseException");
5131 }
5132
5133 #[tokio::test]
5134 async fn delete_cluster_cascades_subresources() {
5135 let svc = EksService::new(make_state());
5136 create_cluster(&svc, "cc").await;
5137 svc.handle(make_request(
5138 Method::POST,
5139 "/clusters/cc/node-groups",
5140 &nodegroup_body("ng"),
5141 ))
5142 .await
5143 .unwrap();
5144 svc.handle(make_request(
5145 Method::DELETE,
5146 "/clusters/cc/node-groups/ng",
5147 "",
5148 ))
5149 .await
5150 .unwrap();
5151 svc.handle(make_request(Method::DELETE, "/clusters/cc", ""))
5152 .await
5153 .unwrap();
5154 create_cluster(&svc, "cc").await;
5156 let resp = svc
5157 .handle(make_request(Method::GET, "/clusters/cc/node-groups", ""))
5158 .await
5159 .unwrap();
5160 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5161 assert_eq!(v["nodegroups"], json!([]));
5162 }
5163
5164 #[tokio::test]
5165 async fn describe_cluster_versions_filters_by_version_status() {
5166 let svc = EksService::new(make_state());
5167 let resp = svc
5168 .handle(make_request(
5169 Method::GET,
5170 "/cluster-versions?versionStatus=EXTENDED_SUPPORT",
5171 "",
5172 ))
5173 .await
5174 .unwrap();
5175 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5176 let rows = v["clusterVersions"].as_array().unwrap();
5177 assert!(!rows.is_empty());
5178 assert!(rows
5179 .iter()
5180 .all(|r| r["versionStatus"] == "EXTENDED_SUPPORT"));
5181 }
5182
5183 fn nodegroup_body(name: &str) -> String {
5188 json!({
5189 "nodegroupName": name,
5190 "nodeRole": "arn:aws:iam::111122223333:role/eks-node",
5191 "subnets": ["subnet-1", "subnet-2"],
5192 "scalingConfig": { "minSize": 1, "maxSize": 3, "desiredSize": 2 },
5193 "instanceTypes": ["t3.large"],
5194 "labels": { "team": "core" }
5195 })
5196 .to_string()
5197 }
5198
5199 async fn create_cluster(svc: &EksService, name: &str) {
5200 svc.handle(make_request(Method::POST, "/clusters", &create_body(name)))
5201 .await
5202 .unwrap();
5203 }
5204
5205 #[tokio::test]
5206 async fn nodegroup_create_describe_list_delete() {
5207 let svc = EksService::new(make_state());
5208 create_cluster(&svc, "c1").await;
5209
5210 let resp = svc
5211 .handle(make_request(
5212 Method::POST,
5213 "/clusters/c1/node-groups",
5214 &nodegroup_body("ng1"),
5215 ))
5216 .await
5217 .unwrap();
5218 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5219 assert_eq!(v["nodegroup"]["nodegroupName"], "ng1");
5220 assert_eq!(v["nodegroup"]["status"], "CREATING");
5221 assert_eq!(v["nodegroup"]["clusterName"], "c1");
5222 assert_eq!(v["nodegroup"]["scalingConfig"]["maxSize"], 3);
5223 assert!(v["nodegroup"]["nodegroupArn"]
5224 .as_str()
5225 .unwrap()
5226 .starts_with("arn:aws:eks:us-east-1:111122223333:nodegroup/c1/ng1/"));
5227
5228 let resp = svc
5230 .handle(make_request(
5231 Method::GET,
5232 "/clusters/c1/node-groups/ng1",
5233 "",
5234 ))
5235 .await
5236 .unwrap();
5237 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5238 assert_eq!(v["nodegroup"]["status"], "ACTIVE");
5239 assert_eq!(v["nodegroup"]["labels"]["team"], "core");
5240
5241 let resp = svc
5242 .handle(make_request(Method::GET, "/clusters/c1/node-groups", ""))
5243 .await
5244 .unwrap();
5245 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5246 assert_eq!(v["nodegroups"], json!(["ng1"]));
5247
5248 let resp = svc
5249 .handle(make_request(
5250 Method::DELETE,
5251 "/clusters/c1/node-groups/ng1",
5252 "",
5253 ))
5254 .await
5255 .unwrap();
5256 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5257 assert_eq!(v["nodegroup"]["status"], "DELETING");
5258
5259 let err = svc
5260 .handle(make_request(
5261 Method::GET,
5262 "/clusters/c1/node-groups/ng1",
5263 "",
5264 ))
5265 .await
5266 .err()
5267 .unwrap();
5268 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5269 assert_eq!(err.code(), "ResourceNotFoundException");
5270 }
5271
5272 #[tokio::test]
5273 async fn nodegroup_on_missing_cluster_is_not_found() {
5274 let svc = EksService::new(make_state());
5275 let err = svc
5276 .handle(make_request(
5277 Method::POST,
5278 "/clusters/ghost/node-groups",
5279 &nodegroup_body("ng1"),
5280 ))
5281 .await
5282 .err()
5283 .unwrap();
5284 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5285 assert_eq!(err.code(), "ResourceNotFoundException");
5286 }
5287
5288 #[tokio::test]
5289 async fn nodegroup_duplicate_is_in_use() {
5290 let svc = EksService::new(make_state());
5291 create_cluster(&svc, "c1").await;
5292 svc.handle(make_request(
5293 Method::POST,
5294 "/clusters/c1/node-groups",
5295 &nodegroup_body("ng1"),
5296 ))
5297 .await
5298 .unwrap();
5299 let err = svc
5300 .handle(make_request(
5301 Method::POST,
5302 "/clusters/c1/node-groups",
5303 &nodegroup_body("ng1"),
5304 ))
5305 .await
5306 .err()
5307 .unwrap();
5308 assert_eq!(err.status(), StatusCode::CONFLICT);
5309 assert_eq!(err.code(), "ResourceInUseException");
5310 }
5311
5312 #[tokio::test]
5313 async fn nodegroup_cross_cluster_isolation() {
5314 let svc = EksService::new(make_state());
5315 create_cluster(&svc, "c1").await;
5316 create_cluster(&svc, "c2").await;
5317 svc.handle(make_request(
5318 Method::POST,
5319 "/clusters/c1/node-groups",
5320 &nodegroup_body("ng1"),
5321 ))
5322 .await
5323 .unwrap();
5324
5325 let resp = svc
5327 .handle(make_request(Method::GET, "/clusters/c2/node-groups", ""))
5328 .await
5329 .unwrap();
5330 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5331 assert_eq!(v["nodegroups"], json!([]));
5332
5333 let err = svc
5334 .handle(make_request(
5335 Method::GET,
5336 "/clusters/c2/node-groups/ng1",
5337 "",
5338 ))
5339 .await
5340 .err()
5341 .unwrap();
5342 assert_eq!(err.code(), "ResourceNotFoundException");
5343 }
5344
5345 #[tokio::test]
5346 async fn nodegroup_update_config_and_version_have_own_updates() {
5347 let svc = EksService::new(make_state());
5348 create_cluster(&svc, "c1").await;
5349 svc.handle(make_request(
5350 Method::POST,
5351 "/clusters/c1/node-groups",
5352 &nodegroup_body("ng1"),
5353 ))
5354 .await
5355 .unwrap();
5356
5357 let resp = svc
5359 .handle(make_request(
5360 Method::POST,
5361 "/clusters/c1/node-groups/ng1/update-version",
5362 &json!({ "version": "1.30" }).to_string(),
5363 ))
5364 .await
5365 .unwrap();
5366 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5367 assert_eq!(v["update"]["type"], "VersionUpdate");
5368 let update_id = v["update"]["id"].as_str().unwrap().to_string();
5369
5370 svc.handle(make_request(
5372 Method::POST,
5373 "/clusters/c1/node-groups/ng1/update-config",
5374 &json!({ "scalingConfig": { "minSize": 2, "maxSize": 5, "desiredSize": 3 } })
5375 .to_string(),
5376 ))
5377 .await
5378 .unwrap();
5379
5380 let resp = svc
5382 .handle(make_request(
5383 Method::GET,
5384 "/clusters/c1/updates?nodegroupName=ng1",
5385 "",
5386 ))
5387 .await
5388 .unwrap();
5389 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5390 assert_eq!(v["updateIds"].as_array().unwrap().len(), 2);
5391
5392 let resp = svc
5394 .handle(make_request(Method::GET, "/clusters/c1/updates", ""))
5395 .await
5396 .unwrap();
5397 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5398 assert_eq!(v["updateIds"].as_array().unwrap().len(), 0);
5399
5400 let resp = svc
5402 .handle(make_request(
5403 Method::GET,
5404 &format!("/clusters/c1/updates/{update_id}?nodegroupName=ng1"),
5405 "",
5406 ))
5407 .await
5408 .unwrap();
5409 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5410 assert_eq!(v["update"]["status"], "Successful");
5411
5412 let resp = svc
5414 .handle(make_request(
5415 Method::GET,
5416 "/clusters/c1/node-groups/ng1",
5417 "",
5418 ))
5419 .await
5420 .unwrap();
5421 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5422 assert_eq!(v["nodegroup"]["version"], "1.30");
5423 assert_eq!(v["nodegroup"]["scalingConfig"]["maxSize"], 5);
5424 }
5425
5426 fn fargate_body(name: &str) -> String {
5431 json!({
5432 "fargateProfileName": name,
5433 "podExecutionRoleArn": "arn:aws:iam::111122223333:role/eks-fargate",
5434 "subnets": ["subnet-1"],
5435 "selectors": [{ "namespace": "default", "labels": { "app": "web" } }]
5436 })
5437 .to_string()
5438 }
5439
5440 #[tokio::test]
5441 async fn fargate_create_describe_list_delete() {
5442 let svc = EksService::new(make_state());
5443 create_cluster(&svc, "c1").await;
5444
5445 let resp = svc
5446 .handle(make_request(
5447 Method::POST,
5448 "/clusters/c1/fargate-profiles",
5449 &fargate_body("fp1"),
5450 ))
5451 .await
5452 .unwrap();
5453 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5454 assert_eq!(v["fargateProfile"]["fargateProfileName"], "fp1");
5455 assert_eq!(v["fargateProfile"]["status"], "CREATING");
5456 assert!(v["fargateProfile"]["fargateProfileArn"]
5457 .as_str()
5458 .unwrap()
5459 .starts_with("arn:aws:eks:us-east-1:111122223333:fargateprofile/c1/fp1/"));
5460
5461 let resp = svc
5462 .handle(make_request(
5463 Method::GET,
5464 "/clusters/c1/fargate-profiles/fp1",
5465 "",
5466 ))
5467 .await
5468 .unwrap();
5469 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5470 assert_eq!(v["fargateProfile"]["status"], "ACTIVE");
5471 assert_eq!(v["fargateProfile"]["selectors"][0]["namespace"], "default");
5472
5473 let resp = svc
5474 .handle(make_request(
5475 Method::GET,
5476 "/clusters/c1/fargate-profiles",
5477 "",
5478 ))
5479 .await
5480 .unwrap();
5481 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5482 assert_eq!(v["fargateProfileNames"], json!(["fp1"]));
5483
5484 let resp = svc
5485 .handle(make_request(
5486 Method::DELETE,
5487 "/clusters/c1/fargate-profiles/fp1",
5488 "",
5489 ))
5490 .await
5491 .unwrap();
5492 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5493 assert_eq!(v["fargateProfile"]["status"], "DELETING");
5494
5495 let err = svc
5496 .handle(make_request(
5497 Method::GET,
5498 "/clusters/c1/fargate-profiles/fp1",
5499 "",
5500 ))
5501 .await
5502 .err()
5503 .unwrap();
5504 assert_eq!(err.code(), "ResourceNotFoundException");
5505 }
5506
5507 #[tokio::test]
5508 async fn fargate_on_missing_cluster_is_not_found() {
5509 let svc = EksService::new(make_state());
5510 let err = svc
5511 .handle(make_request(
5512 Method::POST,
5513 "/clusters/ghost/fargate-profiles",
5514 &fargate_body("fp1"),
5515 ))
5516 .await
5517 .err()
5518 .unwrap();
5519 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5520 assert_eq!(err.code(), "ResourceNotFoundException");
5521 }
5522
5523 #[tokio::test]
5524 async fn fargate_duplicate_is_in_use() {
5525 let svc = EksService::new(make_state());
5526 create_cluster(&svc, "c1").await;
5527 svc.handle(make_request(
5528 Method::POST,
5529 "/clusters/c1/fargate-profiles",
5530 &fargate_body("fp1"),
5531 ))
5532 .await
5533 .unwrap();
5534 let err = svc
5535 .handle(make_request(
5536 Method::POST,
5537 "/clusters/c1/fargate-profiles",
5538 &fargate_body("fp1"),
5539 ))
5540 .await
5541 .err()
5542 .unwrap();
5543 assert_eq!(err.status(), StatusCode::CONFLICT);
5544 assert_eq!(err.code(), "ResourceInUseException");
5545 }
5546
5547 #[tokio::test]
5548 async fn unimplemented_subresource_falls_through() {
5549 let svc = EksService::new(make_state());
5553 create_cluster(&svc, "c1").await;
5554 let err = svc
5555 .handle(make_request(Method::GET, "/clusters/c1/insights", ""))
5556 .await
5557 .err()
5558 .unwrap();
5559 assert_eq!(err.code(), "UnknownOperationException");
5560 }
5561
5562 fn addon_body(name: &str) -> String {
5567 json!({
5568 "addonName": name,
5569 "addonVersion": "v1.18.3-eksbuild.2",
5570 "serviceAccountRoleArn": "arn:aws:iam::111122223333:role/eks-addon",
5571 "configurationValues": "{\"replicaCount\":2}",
5572 "tags": { "team": "core" }
5573 })
5574 .to_string()
5575 }
5576
5577 #[tokio::test]
5578 async fn addon_create_describe_list_update_delete() {
5579 let svc = EksService::new(make_state());
5580 create_cluster(&svc, "c1").await;
5581
5582 let resp = svc
5583 .handle(make_request(
5584 Method::POST,
5585 "/clusters/c1/addons",
5586 &addon_body("vpc-cni"),
5587 ))
5588 .await
5589 .unwrap();
5590 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5591 assert_eq!(v["addon"]["addonName"], "vpc-cni");
5592 assert_eq!(v["addon"]["status"], "CREATING");
5593 assert_eq!(v["addon"]["clusterName"], "c1");
5594 assert_eq!(v["addon"]["addonVersion"], "v1.18.3-eksbuild.2");
5595 assert_eq!(v["addon"]["configurationValues"], "{\"replicaCount\":2}");
5596 assert!(v["addon"]["addonArn"]
5597 .as_str()
5598 .unwrap()
5599 .starts_with("arn:aws:eks:us-east-1:111122223333:addon/c1/vpc-cni/"));
5600
5601 let resp = svc
5603 .handle(make_request(Method::GET, "/clusters/c1/addons/vpc-cni", ""))
5604 .await
5605 .unwrap();
5606 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5607 assert_eq!(v["addon"]["status"], "ACTIVE");
5608 assert_eq!(v["addon"]["tags"]["team"], "core");
5609
5610 let resp = svc
5611 .handle(make_request(Method::GET, "/clusters/c1/addons", ""))
5612 .await
5613 .unwrap();
5614 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5615 assert_eq!(v["addons"], json!(["vpc-cni"]));
5616
5617 let resp = svc
5619 .handle(make_request(
5620 Method::POST,
5621 "/clusters/c1/addons/vpc-cni/update",
5622 &json!({ "addonVersion": "v1.18.5-eksbuild.1" }).to_string(),
5623 ))
5624 .await
5625 .unwrap();
5626 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5627 assert_eq!(v["update"]["type"], "AddonUpdate");
5628 assert_eq!(v["update"]["status"], "InProgress");
5629 let update_id = v["update"]["id"].as_str().unwrap().to_string();
5630
5631 let resp = svc
5632 .handle(make_request(
5633 Method::GET,
5634 &format!("/clusters/c1/updates/{update_id}?addonName=vpc-cni"),
5635 "",
5636 ))
5637 .await
5638 .unwrap();
5639 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5640 assert_eq!(v["update"]["status"], "Successful");
5641
5642 let resp = svc
5644 .handle(make_request(Method::GET, "/clusters/c1/addons/vpc-cni", ""))
5645 .await
5646 .unwrap();
5647 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5648 assert_eq!(v["addon"]["addonVersion"], "v1.18.5-eksbuild.1");
5649
5650 let resp = svc
5652 .handle(make_request(
5653 Method::GET,
5654 "/clusters/c1/updates?addonName=vpc-cni",
5655 "",
5656 ))
5657 .await
5658 .unwrap();
5659 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5660 assert_eq!(v["updateIds"].as_array().unwrap().len(), 1);
5661
5662 let resp = svc
5664 .handle(make_request(
5665 Method::DELETE,
5666 "/clusters/c1/addons/vpc-cni",
5667 "",
5668 ))
5669 .await
5670 .unwrap();
5671 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5672 assert_eq!(v["addon"]["status"], "DELETING");
5673
5674 let err = svc
5675 .handle(make_request(Method::GET, "/clusters/c1/addons/vpc-cni", ""))
5676 .await
5677 .err()
5678 .unwrap();
5679 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5680 assert_eq!(err.code(), "ResourceNotFoundException");
5681 }
5682
5683 #[tokio::test]
5684 async fn addon_on_missing_cluster_is_not_found() {
5685 let svc = EksService::new(make_state());
5686 let err = svc
5687 .handle(make_request(
5688 Method::POST,
5689 "/clusters/ghost/addons",
5690 &addon_body("coredns"),
5691 ))
5692 .await
5693 .err()
5694 .unwrap();
5695 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5696 assert_eq!(err.code(), "ResourceNotFoundException");
5697 }
5698
5699 #[tokio::test]
5700 async fn addon_duplicate_is_in_use() {
5701 let svc = EksService::new(make_state());
5702 create_cluster(&svc, "c1").await;
5703 svc.handle(make_request(
5704 Method::POST,
5705 "/clusters/c1/addons",
5706 &addon_body("coredns"),
5707 ))
5708 .await
5709 .unwrap();
5710 let err = svc
5711 .handle(make_request(
5712 Method::POST,
5713 "/clusters/c1/addons",
5714 &addon_body("coredns"),
5715 ))
5716 .await
5717 .err()
5718 .unwrap();
5719 assert_eq!(err.status(), StatusCode::CONFLICT);
5720 assert_eq!(err.code(), "ResourceInUseException");
5721 }
5722
5723 #[tokio::test]
5724 async fn describe_addon_versions_catalog_is_non_empty() {
5725 let svc = EksService::new(make_state());
5726 let resp = svc
5727 .handle(make_request(Method::GET, "/addons/supported-versions", ""))
5728 .await
5729 .unwrap();
5730 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5731 let addons = v["addons"].as_array().unwrap();
5732 assert!(addons.len() >= 5);
5733 let names: Vec<&str> = addons
5734 .iter()
5735 .map(|a| a["addonName"].as_str().unwrap())
5736 .collect();
5737 assert!(names.contains(&"vpc-cni"));
5738 assert!(names.contains(&"coredns"));
5739 assert!(!addons[0]["addonVersions"].as_array().unwrap().is_empty());
5740
5741 let resp = svc
5743 .handle(make_request(
5744 Method::GET,
5745 "/addons/supported-versions?addonName=coredns",
5746 "",
5747 ))
5748 .await
5749 .unwrap();
5750 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5751 let addons = v["addons"].as_array().unwrap();
5752 assert_eq!(addons.len(), 1);
5753 assert_eq!(addons[0]["addonName"], "coredns");
5754 }
5755
5756 #[tokio::test]
5757 async fn describe_addon_configuration_returns_schema() {
5758 let svc = EksService::new(make_state());
5759 let resp = svc
5760 .handle(make_request(
5761 Method::GET,
5762 "/addons/configuration-schemas?addonName=vpc-cni&addonVersion=v1.18.3-eksbuild.2",
5763 "",
5764 ))
5765 .await
5766 .unwrap();
5767 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5768 assert_eq!(v["addonName"], "vpc-cni");
5769 assert_eq!(v["addonVersion"], "v1.18.3-eksbuild.2");
5770 assert!(v["configurationSchema"]
5771 .as_str()
5772 .unwrap()
5773 .contains("$schema"));
5774 assert_eq!(
5775 v["podIdentityConfiguration"][0]["serviceAccount"],
5776 "aws-node"
5777 );
5778 }
5779
5780 const PRINCIPAL: &str = "arn:aws:iam::111122223333:role/dev";
5785
5786 fn access_entry_body() -> String {
5787 json!({
5788 "principalArn": PRINCIPAL,
5789 "kubernetesGroups": ["viewers"],
5790 "tags": { "team": "core" }
5791 })
5792 .to_string()
5793 }
5794
5795 fn url_encode(s: &str) -> String {
5796 s.replace('%', "%25")
5797 .replace(':', "%3A")
5798 .replace('/', "%2F")
5799 }
5800
5801 fn encoded_principal() -> String {
5802 url_encode(PRINCIPAL)
5803 }
5804
5805 #[tokio::test]
5806 async fn access_entry_full_lifecycle() {
5807 let svc = EksService::new(make_state());
5808 create_cluster(&svc, "c1").await;
5809
5810 let resp = svc
5812 .handle(make_request(
5813 Method::POST,
5814 "/clusters/c1/access-entries",
5815 &access_entry_body(),
5816 ))
5817 .await
5818 .unwrap();
5819 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5820 assert_eq!(v["accessEntry"]["principalArn"], PRINCIPAL);
5821 assert_eq!(v["accessEntry"]["clusterName"], "c1");
5822 assert_eq!(v["accessEntry"]["type"], "STANDARD");
5823 assert_eq!(v["accessEntry"]["kubernetesGroups"], json!(["viewers"]));
5824 assert_eq!(v["accessEntry"]["tags"]["team"], "core");
5825 assert!(v["accessEntry"]["accessEntryArn"]
5826 .as_str()
5827 .unwrap()
5828 .starts_with("arn:aws:eks:us-east-1:111122223333:access-entry/c1/role/"));
5829 assert!(v["accessEntry"]["username"]
5830 .as_str()
5831 .unwrap()
5832 .contains("dev"));
5833
5834 let enc = encoded_principal();
5835
5836 let resp = svc
5838 .handle(make_request(
5839 Method::GET,
5840 &format!("/clusters/c1/access-entries/{enc}"),
5841 "",
5842 ))
5843 .await
5844 .unwrap();
5845 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5846 assert_eq!(v["accessEntry"]["principalArn"], PRINCIPAL);
5847
5848 let resp = svc
5850 .handle(make_request(Method::GET, "/clusters/c1/access-entries", ""))
5851 .await
5852 .unwrap();
5853 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5854 assert_eq!(v["accessEntries"], json!([PRINCIPAL]));
5855
5856 let resp = svc
5858 .handle(make_request(
5859 Method::POST,
5860 &format!("/clusters/c1/access-entries/{enc}"),
5861 &json!({ "kubernetesGroups": ["admins"], "username": "custom" }).to_string(),
5862 ))
5863 .await
5864 .unwrap();
5865 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5866 assert_eq!(v["accessEntry"]["kubernetesGroups"], json!(["admins"]));
5867 assert_eq!(v["accessEntry"]["username"], "custom");
5868
5869 let policy = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy";
5871 let resp = svc
5872 .handle(make_request(
5873 Method::POST,
5874 &format!("/clusters/c1/access-entries/{enc}/access-policies"),
5875 &json!({
5876 "policyArn": policy,
5877 "accessScope": { "type": "namespace", "namespaces": ["default"] }
5878 })
5879 .to_string(),
5880 ))
5881 .await
5882 .unwrap();
5883 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5884 assert_eq!(v["clusterName"], "c1");
5885 assert_eq!(v["principalArn"], PRINCIPAL);
5886 assert_eq!(v["associatedAccessPolicy"]["policyArn"], policy);
5887 assert_eq!(
5888 v["associatedAccessPolicy"]["accessScope"]["type"],
5889 "namespace"
5890 );
5891
5892 let resp = svc
5894 .handle(make_request(
5895 Method::GET,
5896 &format!("/clusters/c1/access-entries/{enc}/access-policies"),
5897 "",
5898 ))
5899 .await
5900 .unwrap();
5901 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5902 assert_eq!(v["clusterName"], "c1");
5903 assert_eq!(v["associatedAccessPolicies"].as_array().unwrap().len(), 1);
5904 assert_eq!(v["associatedAccessPolicies"][0]["policyArn"], policy);
5905
5906 let resp = svc
5908 .handle(make_request(
5909 Method::GET,
5910 &format!("/clusters/c1/access-entries?associatedPolicyArn={policy}"),
5911 "",
5912 ))
5913 .await
5914 .unwrap();
5915 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5916 assert_eq!(v["accessEntries"], json!([PRINCIPAL]));
5917
5918 let enc_policy = encoded_principal_of(policy);
5920 let resp = svc
5921 .handle(make_request(
5922 Method::DELETE,
5923 &format!("/clusters/c1/access-entries/{enc}/access-policies/{enc_policy}"),
5924 "",
5925 ))
5926 .await
5927 .unwrap();
5928 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5929 assert_eq!(v, json!({}));
5930
5931 let resp = svc
5932 .handle(make_request(
5933 Method::GET,
5934 &format!("/clusters/c1/access-entries/{enc}/access-policies"),
5935 "",
5936 ))
5937 .await
5938 .unwrap();
5939 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5940 assert_eq!(v["associatedAccessPolicies"].as_array().unwrap().len(), 0);
5941
5942 let resp = svc
5944 .handle(make_request(
5945 Method::DELETE,
5946 &format!("/clusters/c1/access-entries/{enc}"),
5947 "",
5948 ))
5949 .await
5950 .unwrap();
5951 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
5952 assert_eq!(v, json!({}));
5953
5954 let err = svc
5955 .handle(make_request(
5956 Method::GET,
5957 &format!("/clusters/c1/access-entries/{enc}"),
5958 "",
5959 ))
5960 .await
5961 .err()
5962 .unwrap();
5963 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5964 assert_eq!(err.code(), "ResourceNotFoundException");
5965 }
5966
5967 fn encoded_principal_of(s: &str) -> String {
5968 url_encode(s)
5969 }
5970
5971 #[tokio::test]
5972 async fn access_entry_on_missing_cluster_is_not_found() {
5973 let svc = EksService::new(make_state());
5974 let err = svc
5975 .handle(make_request(
5976 Method::POST,
5977 "/clusters/ghost/access-entries",
5978 &access_entry_body(),
5979 ))
5980 .await
5981 .err()
5982 .unwrap();
5983 assert_eq!(err.status(), StatusCode::NOT_FOUND);
5984 assert_eq!(err.code(), "ResourceNotFoundException");
5985 }
5986
5987 #[tokio::test]
5988 async fn access_entry_duplicate_is_in_use() {
5989 let svc = EksService::new(make_state());
5990 create_cluster(&svc, "c1").await;
5991 svc.handle(make_request(
5992 Method::POST,
5993 "/clusters/c1/access-entries",
5994 &access_entry_body(),
5995 ))
5996 .await
5997 .unwrap();
5998 let err = svc
5999 .handle(make_request(
6000 Method::POST,
6001 "/clusters/c1/access-entries",
6002 &access_entry_body(),
6003 ))
6004 .await
6005 .err()
6006 .unwrap();
6007 assert_eq!(err.status(), StatusCode::CONFLICT);
6008 assert_eq!(err.code(), "ResourceInUseException");
6009 }
6010
6011 #[tokio::test]
6012 async fn describe_access_entry_missing_is_not_found() {
6013 let svc = EksService::new(make_state());
6014 create_cluster(&svc, "c1").await;
6015 let err = svc
6016 .handle(make_request(
6017 Method::GET,
6018 &format!("/clusters/c1/access-entries/{}", encoded_principal()),
6019 "",
6020 ))
6021 .await
6022 .err()
6023 .unwrap();
6024 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6025 assert_eq!(err.code(), "ResourceNotFoundException");
6026 }
6027
6028 #[tokio::test]
6029 async fn list_access_policies_catalog_is_non_empty() {
6030 let svc = EksService::new(make_state());
6031 let resp = svc
6032 .handle(make_request(Method::GET, "/access-policies", ""))
6033 .await
6034 .unwrap();
6035 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6036 let policies = v["accessPolicies"].as_array().unwrap();
6037 assert!(policies.len() >= 10);
6038 let names: Vec<&str> = policies
6039 .iter()
6040 .map(|p| p["name"].as_str().unwrap())
6041 .collect();
6042 assert!(names.contains(&"AmazonEKSClusterAdminPolicy"));
6043 assert!(names.contains(&"AmazonEKSViewPolicy"));
6044 assert!(policies[0]["arn"]
6045 .as_str()
6046 .unwrap()
6047 .starts_with("arn:aws:eks::aws:cluster-access-policy/"));
6048 }
6049
6050 fn idp_body(name: &str) -> String {
6055 json!({
6056 "oidc": {
6057 "identityProviderConfigName": name,
6058 "issuerUrl": "https://example.com",
6059 "clientId": "kubernetes",
6060 "usernameClaim": "email",
6061 "groupsClaim": "groups"
6062 },
6063 "tags": { "team": "core" }
6064 })
6065 .to_string()
6066 }
6067
6068 #[tokio::test]
6069 async fn idp_associate_describe_list_disassociate() {
6070 let svc = EksService::new(make_state());
6071 create_cluster(&svc, "c1").await;
6072
6073 let resp = svc
6075 .handle(make_request(
6076 Method::POST,
6077 "/clusters/c1/identity-provider-configs/associate",
6078 &idp_body("oidc1"),
6079 ))
6080 .await
6081 .unwrap();
6082 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6083 assert_eq!(v["update"]["type"], "AssociateIdentityProviderConfig");
6084 assert_eq!(v["update"]["status"], "InProgress");
6085 assert_eq!(v["tags"]["team"], "core");
6086 let update_id = v["update"]["id"].as_str().unwrap().to_string();
6087
6088 let resp = svc
6090 .handle(make_request(
6091 Method::GET,
6092 &format!("/clusters/c1/updates/{update_id}"),
6093 "",
6094 ))
6095 .await
6096 .unwrap();
6097 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6098 assert_eq!(v["update"]["status"], "Successful");
6099
6100 let describe =
6102 json!({ "identityProviderConfig": { "type": "oidc", "name": "oidc1" } }).to_string();
6103 let resp = svc
6104 .handle(make_request(
6105 Method::POST,
6106 "/clusters/c1/identity-provider-configs/describe",
6107 &describe,
6108 ))
6109 .await
6110 .unwrap();
6111 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6112 let oidc = &v["identityProviderConfig"]["oidc"];
6113 assert_eq!(oidc["identityProviderConfigName"], "oidc1");
6114 assert_eq!(oidc["issuerUrl"], "https://example.com");
6115 assert_eq!(oidc["clientId"], "kubernetes");
6116 assert_eq!(oidc["usernameClaim"], "email");
6117 assert_eq!(oidc["status"], "ACTIVE");
6118 assert!(oidc["identityProviderConfigArn"]
6119 .as_str()
6120 .unwrap()
6121 .starts_with(
6122 "arn:aws:eks:us-east-1:111122223333:identityproviderconfig/c1/oidc/oidc1/"
6123 ));
6124
6125 let resp = svc
6127 .handle(make_request(
6128 Method::GET,
6129 "/clusters/c1/identity-provider-configs",
6130 "",
6131 ))
6132 .await
6133 .unwrap();
6134 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6135 assert_eq!(
6136 v["identityProviderConfigs"],
6137 json!([{ "type": "oidc", "name": "oidc1" }])
6138 );
6139
6140 let resp = svc
6142 .handle(make_request(
6143 Method::POST,
6144 "/clusters/c1/identity-provider-configs/disassociate",
6145 &describe,
6146 ))
6147 .await
6148 .unwrap();
6149 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6150 assert_eq!(v["update"]["type"], "DisassociateIdentityProviderConfig");
6151
6152 let resp = svc
6153 .handle(make_request(
6154 Method::GET,
6155 "/clusters/c1/identity-provider-configs",
6156 "",
6157 ))
6158 .await
6159 .unwrap();
6160 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6161 assert_eq!(v["identityProviderConfigs"], json!([]));
6162
6163 let err = svc
6165 .handle(make_request(
6166 Method::POST,
6167 "/clusters/c1/identity-provider-configs/describe",
6168 &describe,
6169 ))
6170 .await
6171 .err()
6172 .unwrap();
6173 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6174 assert_eq!(err.code(), "ResourceNotFoundException");
6175 }
6176
6177 #[tokio::test]
6178 async fn idp_on_missing_cluster_is_not_found() {
6179 let svc = EksService::new(make_state());
6180 let err = svc
6181 .handle(make_request(
6182 Method::POST,
6183 "/clusters/ghost/identity-provider-configs/associate",
6184 &idp_body("oidc1"),
6185 ))
6186 .await
6187 .err()
6188 .unwrap();
6189 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6190 assert_eq!(err.code(), "ResourceNotFoundException");
6191 }
6192
6193 #[tokio::test]
6194 async fn idp_duplicate_is_in_use() {
6195 let svc = EksService::new(make_state());
6196 create_cluster(&svc, "c1").await;
6197 svc.handle(make_request(
6198 Method::POST,
6199 "/clusters/c1/identity-provider-configs/associate",
6200 &idp_body("oidc1"),
6201 ))
6202 .await
6203 .unwrap();
6204 let err = svc
6205 .handle(make_request(
6206 Method::POST,
6207 "/clusters/c1/identity-provider-configs/associate",
6208 &idp_body("oidc1"),
6209 ))
6210 .await
6211 .err()
6212 .unwrap();
6213 assert_eq!(err.status(), StatusCode::CONFLICT);
6214 assert_eq!(err.code(), "ResourceInUseException");
6215 }
6216
6217 fn pod_identity_body(namespace: &str, sa: &str) -> String {
6222 json!({
6223 "namespace": namespace,
6224 "serviceAccount": sa,
6225 "roleArn": "arn:aws:iam::111122223333:role/pod-role",
6226 "tags": { "team": "core" }
6227 })
6228 .to_string()
6229 }
6230
6231 #[tokio::test]
6232 async fn pod_identity_create_describe_list_update_delete() {
6233 let svc = EksService::new(make_state());
6234 create_cluster(&svc, "c1").await;
6235
6236 let resp = svc
6237 .handle(make_request(
6238 Method::POST,
6239 "/clusters/c1/pod-identity-associations",
6240 &pod_identity_body("default", "app"),
6241 ))
6242 .await
6243 .unwrap();
6244 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6245 let assoc = &v["association"];
6246 assert_eq!(assoc["clusterName"], "c1");
6247 assert_eq!(assoc["namespace"], "default");
6248 assert_eq!(assoc["serviceAccount"], "app");
6249 assert_eq!(assoc["roleArn"], "arn:aws:iam::111122223333:role/pod-role");
6250 assert_eq!(assoc["disableSessionTags"], false);
6251 assert_eq!(assoc["tags"]["team"], "core");
6252 assert!(assoc.get("externalId").is_none());
6254 let association_id = assoc["associationId"].as_str().unwrap().to_string();
6255 assert!(association_id.starts_with("a-"));
6256 assert!(assoc["associationArn"]
6257 .as_str()
6258 .unwrap()
6259 .starts_with("arn:aws:eks:us-east-1:111122223333:podidentityassociation/c1/a-"));
6260
6261 let resp = svc
6263 .handle(make_request(
6264 Method::GET,
6265 &format!("/clusters/c1/pod-identity-associations/{association_id}"),
6266 "",
6267 ))
6268 .await
6269 .unwrap();
6270 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6271 assert_eq!(v["association"]["serviceAccount"], "app");
6272
6273 let resp = svc
6275 .handle(make_request(
6276 Method::GET,
6277 "/clusters/c1/pod-identity-associations?namespace=default",
6278 "",
6279 ))
6280 .await
6281 .unwrap();
6282 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6283 assert_eq!(v["associations"].as_array().unwrap().len(), 1);
6284 assert_eq!(v["associations"][0]["associationId"], association_id);
6285
6286 let resp = svc
6288 .handle(make_request(
6289 Method::GET,
6290 "/clusters/c1/pod-identity-associations?namespace=other",
6291 "",
6292 ))
6293 .await
6294 .unwrap();
6295 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6296 assert_eq!(v["associations"], json!([]));
6297
6298 let resp = svc
6300 .handle(make_request(
6301 Method::POST,
6302 &format!("/clusters/c1/pod-identity-associations/{association_id}"),
6303 &json!({
6304 "roleArn": "arn:aws:iam::111122223333:role/new-role",
6305 "targetRoleArn": "arn:aws:iam::444455556666:role/target",
6306 "disableSessionTags": true
6307 })
6308 .to_string(),
6309 ))
6310 .await
6311 .unwrap();
6312 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6313 assert_eq!(
6314 v["association"]["roleArn"],
6315 "arn:aws:iam::111122223333:role/new-role"
6316 );
6317 assert_eq!(
6318 v["association"]["targetRoleArn"],
6319 "arn:aws:iam::444455556666:role/target"
6320 );
6321 assert_eq!(v["association"]["disableSessionTags"], true);
6322 assert!(v["association"]["externalId"].is_string());
6323
6324 let resp = svc
6326 .handle(make_request(
6327 Method::DELETE,
6328 &format!("/clusters/c1/pod-identity-associations/{association_id}"),
6329 "",
6330 ))
6331 .await
6332 .unwrap();
6333 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6334 assert_eq!(v["association"]["associationId"], association_id);
6335
6336 let err = svc
6337 .handle(make_request(
6338 Method::GET,
6339 &format!("/clusters/c1/pod-identity-associations/{association_id}"),
6340 "",
6341 ))
6342 .await
6343 .err()
6344 .unwrap();
6345 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6346 assert_eq!(err.code(), "ResourceNotFoundException");
6347 }
6348
6349 #[tokio::test]
6350 async fn pod_identity_on_missing_cluster_is_not_found() {
6351 let svc = EksService::new(make_state());
6352 let err = svc
6353 .handle(make_request(
6354 Method::POST,
6355 "/clusters/ghost/pod-identity-associations",
6356 &pod_identity_body("default", "app"),
6357 ))
6358 .await
6359 .err()
6360 .unwrap();
6361 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6362 assert_eq!(err.code(), "ResourceNotFoundException");
6363 }
6364
6365 #[tokio::test]
6366 async fn pod_identity_duplicate_is_in_use() {
6367 let svc = EksService::new(make_state());
6368 create_cluster(&svc, "c1").await;
6369 svc.handle(make_request(
6370 Method::POST,
6371 "/clusters/c1/pod-identity-associations",
6372 &pod_identity_body("default", "app"),
6373 ))
6374 .await
6375 .unwrap();
6376 let err = svc
6377 .handle(make_request(
6378 Method::POST,
6379 "/clusters/c1/pod-identity-associations",
6380 &pod_identity_body("default", "app"),
6381 ))
6382 .await
6383 .err()
6384 .unwrap();
6385 assert_eq!(err.status(), StatusCode::CONFLICT);
6386 assert_eq!(err.code(), "ResourceInUseException");
6387 }
6388
6389 #[tokio::test]
6394 async fn insights_list_describe_and_refresh() {
6395 let svc = EksService::new(make_state());
6396 create_cluster(&svc, "c1").await;
6397
6398 let resp = svc
6400 .handle(make_request(Method::POST, "/clusters/c1/insights", "{}"))
6401 .await
6402 .unwrap();
6403 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6404 let insights = v["insights"].as_array().unwrap();
6405 assert!(!insights.is_empty());
6406 assert_eq!(insights[0]["insightStatus"]["status"], "PASSING");
6407 let id = insights[0]["id"].as_str().unwrap().to_string();
6408
6409 let resp = svc
6411 .handle(make_request(
6412 Method::GET,
6413 &format!("/clusters/c1/insights/{id}"),
6414 "",
6415 ))
6416 .await
6417 .unwrap();
6418 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6419 assert_eq!(v["insight"]["id"], id);
6420 assert_eq!(v["insight"]["category"], "UPGRADE_READINESS");
6421
6422 let resp = svc
6424 .handle(make_request(
6425 Method::POST,
6426 "/clusters/c1/insights-refresh",
6427 "{}",
6428 ))
6429 .await
6430 .unwrap();
6431 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6432 assert_eq!(v["status"], "IN_PROGRESS");
6433 assert!(v.get("startedAt").is_none());
6434
6435 let resp = svc
6437 .handle(make_request(
6438 Method::GET,
6439 "/clusters/c1/insights-refresh",
6440 "",
6441 ))
6442 .await
6443 .unwrap();
6444 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6445 assert_eq!(v["status"], "COMPLETED");
6446 assert!(v.get("endedAt").is_some());
6447 }
6448
6449 #[tokio::test]
6450 async fn insights_on_missing_cluster_is_not_found() {
6451 let svc = EksService::new(make_state());
6452 let err = svc
6453 .handle(make_request(Method::POST, "/clusters/ghost/insights", "{}"))
6454 .await
6455 .err()
6456 .unwrap();
6457 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6458 assert_eq!(err.code(), "ResourceNotFoundException");
6459 }
6460
6461 #[tokio::test]
6462 async fn describe_insight_missing_is_not_found() {
6463 let svc = EksService::new(make_state());
6464 create_cluster(&svc, "c1").await;
6465 let err = svc
6466 .handle(make_request(Method::GET, "/clusters/c1/insights/ghost", ""))
6467 .await
6468 .err()
6469 .unwrap();
6470 assert_eq!(err.code(), "ResourceNotFoundException");
6471 }
6472
6473 #[tokio::test]
6478 async fn associate_encryption_config_mints_update() {
6479 let svc = EksService::new(make_state());
6480 create_cluster(&svc, "c1").await;
6481 let body = json!({
6482 "encryptionConfig": [{
6483 "resources": ["secrets"],
6484 "provider": { "keyArn": "arn:aws:kms:us-east-1:111122223333:key/abc" }
6485 }]
6486 })
6487 .to_string();
6488 let resp = svc
6489 .handle(make_request(
6490 Method::POST,
6491 "/clusters/c1/encryption-config/associate",
6492 &body,
6493 ))
6494 .await
6495 .unwrap();
6496 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6497 assert_eq!(v["update"]["type"], "AssociateEncryptionConfig");
6498 assert_eq!(v["update"]["status"], "InProgress");
6499 let update_id = v["update"]["id"].as_str().unwrap().to_string();
6500
6501 let resp = svc
6504 .handle(make_request(
6505 Method::POST,
6506 &format!("/clusters/c1/updates/{update_id}/cancel-update"),
6507 "{}",
6508 ))
6509 .await
6510 .unwrap();
6511 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6512 assert_eq!(v["update"]["status"], "Cancelled");
6513 }
6514
6515 #[tokio::test]
6516 async fn encryption_config_on_missing_cluster_is_not_found() {
6517 let svc = EksService::new(make_state());
6518 let err = svc
6519 .handle(make_request(
6520 Method::POST,
6521 "/clusters/ghost/encryption-config/associate",
6522 &json!({ "encryptionConfig": [] }).to_string(),
6523 ))
6524 .await
6525 .err()
6526 .unwrap();
6527 assert_eq!(err.code(), "ResourceNotFoundException");
6528 }
6529
6530 #[tokio::test]
6531 async fn register_and_deregister_cluster() {
6532 let svc = EksService::new(make_state());
6533 let body = json!({
6534 "name": "connected1",
6535 "connectorConfig": {
6536 "roleArn": "arn:aws:iam::111122223333:role/eks-connector",
6537 "provider": "EKS_ANYWHERE"
6538 }
6539 })
6540 .to_string();
6541 let resp = svc
6542 .handle(make_request(Method::POST, "/cluster-registrations", &body))
6543 .await
6544 .unwrap();
6545 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6546 assert_eq!(v["cluster"]["name"], "connected1");
6547 assert_eq!(v["cluster"]["status"], "PENDING");
6548 assert_eq!(v["cluster"]["connectorConfig"]["provider"], "EKS_ANYWHERE");
6549 assert!(v["cluster"]["connectorConfig"]["activationId"].is_string());
6550
6551 let err = svc
6553 .handle(make_request(Method::POST, "/cluster-registrations", &body))
6554 .await
6555 .err()
6556 .unwrap();
6557 assert_eq!(err.code(), "ResourceInUseException");
6558
6559 let resp = svc
6561 .handle(make_request(
6562 Method::DELETE,
6563 "/cluster-registrations/connected1",
6564 "",
6565 ))
6566 .await
6567 .unwrap();
6568 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6569 assert_eq!(v["cluster"]["status"], "DELETING");
6570
6571 let err = svc
6572 .handle(make_request(
6573 Method::DELETE,
6574 "/cluster-registrations/connected1",
6575 "",
6576 ))
6577 .await
6578 .err()
6579 .unwrap();
6580 assert_eq!(err.code(), "ResourceNotFoundException");
6581 }
6582
6583 #[tokio::test]
6584 async fn deregister_non_connected_cluster_is_not_found() {
6585 let svc = EksService::new(make_state());
6586 create_cluster(&svc, "regular").await;
6587 let err = svc
6589 .handle(make_request(
6590 Method::DELETE,
6591 "/cluster-registrations/regular",
6592 "",
6593 ))
6594 .await
6595 .err()
6596 .unwrap();
6597 assert_eq!(err.code(), "ResourceNotFoundException");
6598 }
6599
6600 #[tokio::test]
6601 async fn describe_cluster_versions_catalog_is_non_empty() {
6602 let svc = EksService::new(make_state());
6603 let resp = svc
6604 .handle(make_request(Method::GET, "/cluster-versions", ""))
6605 .await
6606 .unwrap();
6607 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6608 let versions = v["clusterVersions"].as_array().unwrap();
6609 assert!(versions.len() >= 5);
6610 let names: Vec<&str> = versions
6611 .iter()
6612 .map(|x| x["clusterVersion"].as_str().unwrap())
6613 .collect();
6614 assert!(names.contains(&"1.31"));
6615 assert_eq!(
6617 versions
6618 .iter()
6619 .filter(|x| x["defaultVersion"] == true)
6620 .count(),
6621 1
6622 );
6623 assert_eq!(versions[0]["defaultVersion"], true);
6625
6626 let resp = svc
6628 .handle(make_request(
6629 Method::GET,
6630 "/cluster-versions?defaultOnly=true",
6631 "",
6632 ))
6633 .await
6634 .unwrap();
6635 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6636 assert_eq!(v["clusterVersions"].as_array().unwrap().len(), 1);
6637 assert_eq!(v["clusterVersions"][0]["clusterVersion"], "1.31");
6638 }
6639
6640 #[tokio::test]
6641 async fn describe_cluster_versions_rejects_bad_maxresults() {
6642 let svc = EksService::new(make_state());
6643 let err = svc
6644 .handle(make_request(
6645 Method::GET,
6646 "/cluster-versions?maxResults=0",
6647 "",
6648 ))
6649 .await
6650 .err()
6651 .unwrap();
6652 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
6653 assert_eq!(err.code(), "InvalidParameterException");
6654 }
6655
6656 fn capability_body(name: &str) -> String {
6661 json!({
6662 "capabilityName": name,
6663 "type": "ACK",
6664 "roleArn": "arn:aws:iam::111122223333:role/eks-capability",
6665 "deletePropagationPolicy": "RETAIN",
6666 "tags": { "team": "core" }
6667 })
6668 .to_string()
6669 }
6670
6671 #[tokio::test]
6672 async fn capability_create_describe_list_update_delete() {
6673 let svc = EksService::new(make_state());
6674 create_cluster(&svc, "c1").await;
6675
6676 let resp = svc
6677 .handle(make_request(
6678 Method::POST,
6679 "/clusters/c1/capabilities",
6680 &capability_body("ack"),
6681 ))
6682 .await
6683 .unwrap();
6684 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6685 assert_eq!(v["capability"]["capabilityName"], "ack");
6686 assert_eq!(v["capability"]["status"], "CREATING");
6687 assert_eq!(v["capability"]["type"], "ACK");
6688 assert!(v["capability"]["arn"]
6689 .as_str()
6690 .unwrap()
6691 .starts_with("arn:aws:eks:us-east-1:111122223333:capability/c1/ack/"));
6692
6693 let resp = svc
6695 .handle(make_request(
6696 Method::GET,
6697 "/clusters/c1/capabilities/ack",
6698 "",
6699 ))
6700 .await
6701 .unwrap();
6702 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6703 assert_eq!(v["capability"]["status"], "ACTIVE");
6704 assert_eq!(v["capability"]["tags"]["team"], "core");
6705
6706 let resp = svc
6707 .handle(make_request(Method::GET, "/clusters/c1/capabilities", ""))
6708 .await
6709 .unwrap();
6710 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6711 assert_eq!(v["capabilities"].as_array().unwrap().len(), 1);
6712 assert_eq!(v["capabilities"][0]["capabilityName"], "ack");
6713
6714 let resp = svc
6716 .handle(make_request(
6717 Method::POST,
6718 "/clusters/c1/capabilities/ack",
6719 &json!({ "roleArn": "arn:aws:iam::111122223333:role/new" }).to_string(),
6720 ))
6721 .await
6722 .unwrap();
6723 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6724 assert_eq!(v["update"]["type"], "CapabilityUpdate");
6725
6726 let resp = svc
6728 .handle(make_request(
6729 Method::DELETE,
6730 "/clusters/c1/capabilities/ack",
6731 "",
6732 ))
6733 .await
6734 .unwrap();
6735 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6736 assert_eq!(v["capability"]["status"], "DELETING");
6737
6738 let err = svc
6739 .handle(make_request(
6740 Method::GET,
6741 "/clusters/c1/capabilities/ack",
6742 "",
6743 ))
6744 .await
6745 .err()
6746 .unwrap();
6747 assert_eq!(err.code(), "ResourceNotFoundException");
6748 }
6749
6750 #[tokio::test]
6751 async fn capability_on_missing_cluster_is_not_found() {
6752 let svc = EksService::new(make_state());
6753 let err = svc
6754 .handle(make_request(
6755 Method::POST,
6756 "/clusters/ghost/capabilities",
6757 &capability_body("ack"),
6758 ))
6759 .await
6760 .err()
6761 .unwrap();
6762 assert_eq!(err.code(), "ResourceNotFoundException");
6763 }
6764
6765 #[tokio::test]
6766 async fn capability_duplicate_is_in_use() {
6767 let svc = EksService::new(make_state());
6768 create_cluster(&svc, "c1").await;
6769 svc.handle(make_request(
6770 Method::POST,
6771 "/clusters/c1/capabilities",
6772 &capability_body("ack"),
6773 ))
6774 .await
6775 .unwrap();
6776 let err = svc
6777 .handle(make_request(
6778 Method::POST,
6779 "/clusters/c1/capabilities",
6780 &capability_body("ack"),
6781 ))
6782 .await
6783 .err()
6784 .unwrap();
6785 assert_eq!(err.status(), StatusCode::CONFLICT);
6786 assert_eq!(err.code(), "ResourceInUseException");
6787 }
6788
6789 fn subscription_body(name: &str) -> String {
6794 json!({
6795 "name": name,
6796 "term": { "duration": 12, "unit": "MONTHS" },
6797 "licenseQuantity": 5,
6798 "licenseType": "Cluster",
6799 "autoRenew": false,
6800 "tags": { "team": "core" }
6801 })
6802 .to_string()
6803 }
6804
6805 #[tokio::test]
6806 async fn subscription_create_describe_list_update_delete() {
6807 let svc = EksService::new(make_state());
6808
6809 let resp = svc
6810 .handle(make_request(
6811 Method::POST,
6812 "/eks-anywhere-subscriptions",
6813 &subscription_body("sub1"),
6814 ))
6815 .await
6816 .unwrap();
6817 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6818 let sub = &v["subscription"];
6819 assert_eq!(sub["status"], "ACTIVE");
6820 assert_eq!(sub["licenseQuantity"], 5);
6821 assert_eq!(sub["term"]["duration"], 12);
6822 assert_eq!(sub["autoRenew"], false);
6823 let id = sub["id"].as_str().unwrap().to_string();
6824 assert!(sub["arn"]
6825 .as_str()
6826 .unwrap()
6827 .starts_with("arn:aws:eks:us-east-1:111122223333:eks-anywhere-subscription/"));
6828
6829 let resp = svc
6831 .handle(make_request(
6832 Method::GET,
6833 &format!("/eks-anywhere-subscriptions/{id}"),
6834 "",
6835 ))
6836 .await
6837 .unwrap();
6838 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6839 assert_eq!(v["subscription"]["id"], id);
6840
6841 let resp = svc
6843 .handle(make_request(Method::GET, "/eks-anywhere-subscriptions", ""))
6844 .await
6845 .unwrap();
6846 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6847 assert_eq!(v["subscriptions"].as_array().unwrap().len(), 1);
6848
6849 let resp = svc
6851 .handle(make_request(
6852 Method::POST,
6853 &format!("/eks-anywhere-subscriptions/{id}"),
6854 &json!({ "autoRenew": true }).to_string(),
6855 ))
6856 .await
6857 .unwrap();
6858 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6859 assert_eq!(v["subscription"]["autoRenew"], true);
6860
6861 let resp = svc
6863 .handle(make_request(
6864 Method::DELETE,
6865 &format!("/eks-anywhere-subscriptions/{id}"),
6866 "",
6867 ))
6868 .await
6869 .unwrap();
6870 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
6871 assert_eq!(v["subscription"]["status"], "DELETING");
6872
6873 let err = svc
6874 .handle(make_request(
6875 Method::GET,
6876 &format!("/eks-anywhere-subscriptions/{id}"),
6877 "",
6878 ))
6879 .await
6880 .err()
6881 .unwrap();
6882 assert_eq!(err.code(), "ResourceNotFoundException");
6883 }
6884
6885 #[tokio::test]
6886 async fn subscription_describe_missing_is_not_found() {
6887 let svc = EksService::new(make_state());
6888 let err = svc
6889 .handle(make_request(
6890 Method::GET,
6891 "/eks-anywhere-subscriptions/ghost",
6892 "",
6893 ))
6894 .await
6895 .err()
6896 .unwrap();
6897 assert_eq!(err.status(), StatusCode::NOT_FOUND);
6898 assert_eq!(err.code(), "ResourceNotFoundException");
6899 }
6900
6901 #[tokio::test]
6902 async fn subscription_rejects_bad_name() {
6903 let svc = EksService::new(make_state());
6904 let err = svc
6905 .handle(make_request(
6906 Method::POST,
6907 "/eks-anywhere-subscriptions",
6908 &json!({ "name": "-bad", "term": { "duration": 1, "unit": "MONTHS" } }).to_string(),
6909 ))
6910 .await
6911 .err()
6912 .unwrap();
6913 assert_eq!(err.code(), "InvalidParameterException");
6914 }
6915}