Skip to main content

fakecloud_eks/
state.rs

1//! Account-partitioned, serializable state for AWS EKS.
2//!
3//! Models the EKS cluster control plane: clusters (with their VPC config,
4//! logging, Kubernetes network config, and tags) plus the per-cluster `Update`
5//! objects produced by `UpdateClusterConfig` / `UpdateClusterVersion` and read
6//! back through `DescribeUpdate` / `ListUpdates`; managed node groups and
7//! Fargate profiles (both cluster sub-resources, each with their own status
8//! lifecycle, and node groups with their own update history), add-ons, access
9//! entries, OIDC identity-provider configs, and Pod Identity associations (all
10//! cluster sub-resources).
11
12use std::collections::BTreeMap;
13use std::sync::Arc;
14
15use chrono::{DateTime, Utc};
16use parking_lot::RwLock;
17use serde::{Deserialize, Serialize};
18
19use fakecloud_core::multi_account::{AccountState, MultiAccountState};
20
21pub const EKS_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
22
23/// The default Kubernetes version a cluster is created with when the caller
24/// omits `version`.
25pub const DEFAULT_K8S_VERSION: &str = "1.31";
26
27/// Tags on a resource, stored by ARN so `TagResource` / `UntagResource` /
28/// `ListTagsForResource` work uniformly.
29pub type TagMap = BTreeMap<String, String>;
30
31/// A single control-plane update produced by `UpdateClusterConfig` or
32/// `UpdateClusterVersion`. `params` preserves the `(type, value)` pairs AWS
33/// reports back so `DescribeUpdate` round-trips faithfully.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Update {
36    pub id: String,
37    pub status: String,
38    pub type_: String,
39    pub params: Vec<(String, String)>,
40    pub created_at: DateTime<Utc>,
41}
42
43/// A managed node group, a sub-resource of a cluster. Complex members
44/// (scaling config, subnets, labels, taints, ...) are stored as the
45/// `VpcConfigResponse`-style JSON objects echoed back on every describe so
46/// `DescribeNodegroup` round-trips faithfully.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Nodegroup {
49    pub name: String,
50    pub arn: String,
51    pub cluster_name: String,
52    pub version: String,
53    pub release_version: String,
54    pub status: String,
55    pub capacity_type: String,
56    pub ami_type: String,
57    pub node_role: String,
58    pub created_at: DateTime<Utc>,
59    pub modified_at: DateTime<Utc>,
60    pub disk_size: i64,
61    pub scaling_config: serde_json::Value,
62    pub update_config: serde_json::Value,
63    pub instance_types: serde_json::Value,
64    pub subnets: serde_json::Value,
65    pub labels: serde_json::Value,
66    pub taints: serde_json::Value,
67    /// Present only when the caller supplied `remoteAccess`.
68    pub remote_access: Option<serde_json::Value>,
69    /// Present only when the caller supplied `launchTemplate`.
70    pub launch_template: Option<serde_json::Value>,
71    /// The synthetic Auto Scaling group name reported under `resources`.
72    pub asg_name: String,
73    pub tags: TagMap,
74    /// Per-nodegroup updates keyed by update id, disambiguated from cluster
75    /// updates by the `nodegroupName` query param on Describe/ListUpdates.
76    pub updates: BTreeMap<String, Update>,
77}
78
79/// An EKS add-on, a sub-resource of a cluster (e.g. `vpc-cni`, `coredns`).
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Addon {
82    pub name: String,
83    pub arn: String,
84    pub cluster_name: String,
85    pub addon_version: String,
86    pub status: String,
87    pub created_at: DateTime<Utc>,
88    pub modified_at: DateTime<Utc>,
89    /// Present only when the caller supplied `serviceAccountRoleArn`.
90    pub service_account_role_arn: Option<String>,
91    /// Present only when the caller supplied `configurationValues`.
92    pub configuration_values: Option<String>,
93    /// The custom install namespace, if the caller supplied `namespaceConfig`.
94    pub namespace: Option<String>,
95    /// Pod identity association ARNs (one per requested association), echoed
96    /// back as the `podIdentityAssociations` StringList on describe.
97    pub pod_identity_associations: Vec<String>,
98    pub tags: TagMap,
99    /// Per-addon updates keyed by update id, disambiguated from cluster and
100    /// node-group updates by the `addonName` query param on Describe/ListUpdates.
101    pub updates: BTreeMap<String, Update>,
102}
103
104/// An access policy associated to an access entry via `AssociateAccessPolicy`.
105/// `access_scope` is stored as the `{type, namespaces}` JSON object echoed back
106/// on describe/list so the round-trip is faithful.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct AssociatedPolicy {
109    pub policy_arn: String,
110    pub access_scope: serde_json::Value,
111    pub associated_at: DateTime<Utc>,
112    pub modified_at: DateTime<Utc>,
113}
114
115/// An EKS access entry, a sub-resource of a cluster keyed by principal ARN.
116/// The access policies associated to the entry are a nested list.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct AccessEntry {
119    pub principal_arn: String,
120    pub cluster_name: String,
121    pub arn: String,
122    pub kubernetes_groups: Vec<String>,
123    pub username: String,
124    pub type_: String,
125    pub created_at: DateTime<Utc>,
126    pub modified_at: DateTime<Utc>,
127    pub tags: TagMap,
128    /// Access policies associated to this entry, keyed by policy ARN order.
129    #[serde(default)]
130    pub associated_policies: Vec<AssociatedPolicy>,
131}
132
133/// An OIDC identity-provider config associated to a cluster via
134/// `AssociateIdentityProviderConfig`, keyed by `identityProviderConfigName`.
135/// Optional OIDC members are stored as `Option`s so describe only echoes the
136/// ones the caller supplied, and `required_claims` keeps the caller's map
137/// verbatim for a faithful round-trip.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct IdentityProviderConfig {
140    pub name: String,
141    pub arn: String,
142    pub cluster_name: String,
143    pub issuer_url: String,
144    pub client_id: String,
145    pub username_claim: Option<String>,
146    pub username_prefix: Option<String>,
147    pub groups_claim: Option<String>,
148    pub groups_prefix: Option<String>,
149    pub required_claims: serde_json::Value,
150    pub status: String,
151    pub tags: TagMap,
152}
153
154/// An EKS Pod Identity association, a sub-resource of a cluster keyed by its
155/// generated `associationId`. Binds a Kubernetes service account (in a
156/// namespace) to an IAM role.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct PodIdentityAssociation {
159    pub cluster_name: String,
160    pub namespace: String,
161    pub service_account: String,
162    pub role_arn: String,
163    pub association_arn: String,
164    pub association_id: String,
165    pub created_at: DateTime<Utc>,
166    pub modified_at: DateTime<Utc>,
167    pub disable_session_tags: bool,
168    /// Present only when the caller supplied `targetRoleArn` (cross-account).
169    pub target_role_arn: Option<String>,
170    /// Generated for the target role's trust policy when `targetRoleArn` is set.
171    pub external_id: Option<String>,
172    pub tags: TagMap,
173}
174
175/// A Fargate profile, a sub-resource of a cluster.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct FargateProfile {
178    pub name: String,
179    pub arn: String,
180    pub cluster_name: String,
181    pub pod_execution_role_arn: String,
182    pub status: String,
183    pub created_at: DateTime<Utc>,
184    pub subnets: serde_json::Value,
185    pub selectors: serde_json::Value,
186    pub tags: TagMap,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct Cluster {
191    pub name: String,
192    pub arn: String,
193    pub version: String,
194    pub role_arn: String,
195    pub status: String,
196    pub created_at: DateTime<Utc>,
197    pub endpoint: String,
198    pub platform_version: String,
199    pub certificate_authority_data: String,
200    /// The `VpcConfigResponse`-shaped object echoed back on every describe.
201    pub resources_vpc_config: serde_json::Value,
202    /// The `KubernetesNetworkConfigResponse`-shaped object, if one applies.
203    pub kubernetes_network_config: serde_json::Value,
204    /// The `Logging` object echoed back (defaults to all types disabled).
205    pub logging: serde_json::Value,
206    pub tags: TagMap,
207    /// Per-cluster updates keyed by update id.
208    pub updates: BTreeMap<String, Update>,
209    /// The `ConnectorConfigResponse`-shaped object present only on clusters
210    /// created via `RegisterCluster` (i.e. connected/external clusters).
211    #[serde(default)]
212    pub connector_config: Option<serde_json::Value>,
213    /// The `EncryptionConfigList` applied via `AssociateEncryptionConfig`,
214    /// echoed back on describe.
215    #[serde(default)]
216    pub encryption_config: Option<serde_json::Value>,
217    /// The `AccessConfigResponse`-shaped object (`{authenticationMode}`) echoed
218    /// back on every describe. Defaults to `CONFIG_MAP` when the caller omits
219    /// `accessConfig` at create time, mirroring the real API default.
220    #[serde(default = "default_access_config")]
221    pub access_config: serde_json::Value,
222    /// The `UpgradePolicyResponse`-shaped object (`{supportType}`) echoed back
223    /// on every describe. Defaults to `EXTENDED` (extended support enabled) when
224    /// the caller omits `upgradePolicy` at create time.
225    #[serde(default = "default_upgrade_policy")]
226    pub upgrade_policy: serde_json::Value,
227    /// The `ComputeConfigResponse` (EKS Auto Mode) echoed back when the caller
228    /// supplied `computeConfig` at create or via `UpdateClusterConfig`.
229    #[serde(default)]
230    pub compute_config: Option<serde_json::Value>,
231    /// The `StorageConfigResponse` (EKS Auto Mode block storage) echoed back
232    /// when supplied.
233    #[serde(default)]
234    pub storage_config: Option<serde_json::Value>,
235    /// The `ZonalShiftConfigResponse` echoed back when supplied.
236    #[serde(default)]
237    pub zonal_shift_config: Option<serde_json::Value>,
238    /// The `RemoteNetworkConfigResponse` (hybrid nodes) echoed back when supplied.
239    #[serde(default)]
240    pub remote_network_config: Option<serde_json::Value>,
241    /// The `ControlPlaneScalingConfigResponse` echoed back when supplied.
242    #[serde(default)]
243    pub control_plane_scaling_config: Option<serde_json::Value>,
244    /// Whether deletion protection is enabled, echoed back when set.
245    #[serde(default)]
246    pub deletion_protection: Option<bool>,
247}
248
249/// The `accessConfig` a cluster reports when none was supplied at create time:
250/// `CONFIG_MAP` authentication (the backward-compatible default).
251pub fn default_access_config() -> serde_json::Value {
252    serde_json::json!({ "authenticationMode": "CONFIG_MAP" })
253}
254
255/// The `upgradePolicy` a cluster reports when none was supplied at create time:
256/// `EXTENDED` support (extended support enabled by default).
257pub fn default_upgrade_policy() -> serde_json::Value {
258    serde_json::json!({ "supportType": "EXTENDED" })
259}
260
261/// An upgrade-readiness/misconfiguration Insight that EKS auto-generates for a
262/// cluster, a cluster sub-resource keyed by its generated id. Read back through
263/// `ListInsights` / `DescribeInsight`.
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct Insight {
266    pub id: String,
267    pub name: String,
268    pub category: String,
269    pub kubernetes_version: String,
270    pub description: String,
271    pub recommendation: String,
272    /// The `InsightStatusValue` (PASSING / WARNING / ERROR / UNKNOWN).
273    pub status: String,
274    pub reason: String,
275    pub last_refresh_time: DateTime<Utc>,
276    pub last_transition_time: DateTime<Utc>,
277}
278
279/// The state of the most recent `StartInsightsRefresh` for a cluster, read back
280/// through `DescribeInsightsRefresh`.
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct InsightsRefresh {
283    /// The `InsightsRefreshStatus` (IN_PROGRESS / FAILED / COMPLETED).
284    pub status: String,
285    pub started_at: DateTime<Utc>,
286    pub ended_at: Option<DateTime<Utc>>,
287}
288
289/// An EKS cluster capability (e.g. an ACK / KRO / ArgoCD controller), a
290/// sub-resource of a cluster keyed by `capabilityName`.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct Capability {
293    pub name: String,
294    pub arn: String,
295    pub cluster_name: String,
296    /// The `CapabilityType` (ACK / KRO / ARGOCD).
297    pub type_: String,
298    pub role_arn: String,
299    pub status: String,
300    pub version: String,
301    /// The `CapabilityConfigurationResponse`-shaped object, when supplied.
302    pub configuration: Option<serde_json::Value>,
303    pub tags: TagMap,
304    pub created_at: DateTime<Utc>,
305    pub modified_at: DateTime<Utc>,
306    pub delete_propagation_policy: Option<String>,
307}
308
309/// An EKS Anywhere subscription, an account-scoped (not cluster-scoped)
310/// resource keyed by its generated id.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct EksAnywhereSubscription {
313    pub id: String,
314    pub arn: String,
315    pub name: String,
316    pub created_at: DateTime<Utc>,
317    pub effective_date: DateTime<Utc>,
318    pub expiration_date: DateTime<Utc>,
319    pub license_quantity: i64,
320    /// The `EksAnywhereSubscriptionLicenseType` (only `Cluster` today).
321    pub license_type: String,
322    pub term_duration: i64,
323    /// The `EksAnywhereSubscriptionTermUnit` (only `MONTHS` today).
324    pub term_unit: String,
325    pub status: String,
326    pub auto_renew: bool,
327    pub tags: TagMap,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize)]
331pub struct EksState {
332    pub clusters: BTreeMap<String, Cluster>,
333    /// Tags keyed by resource ARN. Cluster tags live on the cluster itself;
334    /// this map holds tags applied to any other EKS ARN via `TagResource`.
335    pub tags: BTreeMap<String, TagMap>,
336    /// Node groups nested as cluster name -> nodegroup name -> node group.
337    /// Nesting (rather than a `(String, String)` tuple key) keeps the map
338    /// JSON-serializable through the snapshot and scopes node groups per
339    /// cluster for natural cross-cluster isolation.
340    #[serde(default)]
341    pub nodegroups: BTreeMap<String, BTreeMap<String, Nodegroup>>,
342    /// Fargate profiles nested as cluster name -> profile name -> profile.
343    #[serde(default)]
344    pub fargate_profiles: BTreeMap<String, BTreeMap<String, FargateProfile>>,
345    /// Add-ons nested as cluster name -> addon name -> addon.
346    #[serde(default)]
347    pub addons: BTreeMap<String, BTreeMap<String, Addon>>,
348    /// Access entries nested as cluster name -> principal ARN -> access entry.
349    #[serde(default)]
350    pub access_entries: BTreeMap<String, BTreeMap<String, AccessEntry>>,
351    /// Identity-provider configs nested as cluster name -> config name -> config.
352    #[serde(default)]
353    pub identity_provider_configs: BTreeMap<String, BTreeMap<String, IdentityProviderConfig>>,
354    /// Pod identity associations nested as cluster name -> associationId -> assoc.
355    #[serde(default)]
356    pub pod_identity_associations: BTreeMap<String, BTreeMap<String, PodIdentityAssociation>>,
357    /// Auto-generated Insights nested as cluster name -> insight id -> insight.
358    #[serde(default)]
359    pub insights: BTreeMap<String, BTreeMap<String, Insight>>,
360    /// The most recent insights-refresh state, keyed by cluster name.
361    #[serde(default)]
362    pub insights_refresh: BTreeMap<String, InsightsRefresh>,
363    /// Capabilities nested as cluster name -> capability name -> capability.
364    #[serde(default)]
365    pub capabilities: BTreeMap<String, BTreeMap<String, Capability>>,
366    /// EKS Anywhere subscriptions (account-scoped) keyed by subscription id.
367    #[serde(default)]
368    pub eks_anywhere_subscriptions: BTreeMap<String, EksAnywhereSubscription>,
369}
370
371impl AccountState for EksState {
372    fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
373        Self::default()
374    }
375}
376
377/// Build the ARN for a cluster.
378pub fn cluster_arn(region: &str, account_id: &str, name: &str) -> String {
379    format!("arn:aws:eks:{region}:{account_id}:cluster/{name}")
380}
381
382/// Build the ARN for a node group. AWS appends a random UUID segment after
383/// `nodegroup/{cluster}/{name}` so each node group's ARN is unique even after
384/// a delete/recreate.
385pub fn nodegroup_arn(
386    region: &str,
387    account_id: &str,
388    cluster: &str,
389    name: &str,
390    id: &str,
391) -> String {
392    format!("arn:aws:eks:{region}:{account_id}:nodegroup/{cluster}/{name}/{id}")
393}
394
395/// Build the ARN for a Fargate profile.
396pub fn fargate_profile_arn(
397    region: &str,
398    account_id: &str,
399    cluster: &str,
400    name: &str,
401    id: &str,
402) -> String {
403    format!("arn:aws:eks:{region}:{account_id}:fargateprofile/{cluster}/{name}/{id}")
404}
405
406/// Build the ARN for an add-on. AWS appends a random UUID segment after
407/// `addon/{cluster}/{name}` so each add-on's ARN is unique across recreate.
408pub fn addon_arn(region: &str, account_id: &str, cluster: &str, name: &str, id: &str) -> String {
409    format!("arn:aws:eks:{region}:{account_id}:addon/{cluster}/{name}/{id}")
410}
411
412/// Build the ARN for an access entry. AWS's access-entry ARN embeds the
413/// principal type (`role`/`user`), the account, the principal's resource name,
414/// and a random UUID:
415/// `arn:aws:eks:{region}:{account}:access-entry/{cluster}/{type}/{account}/{name}/{uuid}`.
416pub fn access_entry_arn(
417    region: &str,
418    account_id: &str,
419    cluster: &str,
420    principal_type: &str,
421    principal_name: &str,
422    id: &str,
423) -> String {
424    format!(
425        "arn:aws:eks:{region}:{account_id}:access-entry/{cluster}/{principal_type}/{account_id}/{principal_name}/{id}"
426    )
427}
428
429/// Build the ARN for an OIDC identity-provider config. AWS embeds the config
430/// type (`oidc`), the config name, and a random UUID.
431pub fn identity_provider_config_arn(
432    region: &str,
433    account_id: &str,
434    cluster: &str,
435    name: &str,
436    id: &str,
437) -> String {
438    format!("arn:aws:eks:{region}:{account_id}:identityproviderconfig/{cluster}/oidc/{name}/{id}")
439}
440
441/// Build the ARN for a pod identity association attached to an add-on.
442pub fn pod_identity_association_arn(
443    region: &str,
444    account_id: &str,
445    cluster: &str,
446    id: &str,
447) -> String {
448    format!("arn:aws:eks:{region}:{account_id}:podidentityassociation/{cluster}/a-{id}")
449}
450
451/// Build the ARN for a cluster capability. AWS appends a random UUID segment
452/// after `capability/{cluster}/{name}` so each capability's ARN is unique.
453pub fn capability_arn(
454    region: &str,
455    account_id: &str,
456    cluster: &str,
457    name: &str,
458    id: &str,
459) -> String {
460    format!("arn:aws:eks:{region}:{account_id}:capability/{cluster}/{name}/{id}")
461}
462
463/// Build the ARN for an EKS Anywhere subscription.
464pub fn eks_anywhere_subscription_arn(region: &str, account_id: &str, id: &str) -> String {
465    format!("arn:aws:eks:{region}:{account_id}:eks-anywhere-subscription/{id}")
466}
467
468pub type SharedEksState = Arc<RwLock<MultiAccountState<EksState>>>;
469
470#[derive(Debug, Serialize, Deserialize)]
471pub struct EksSnapshot {
472    pub schema_version: u32,
473    pub accounts: MultiAccountState<EksState>,
474}