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