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}
228
229/// The `accessConfig` a cluster reports when none was supplied at create time:
230/// `CONFIG_MAP` authentication (the backward-compatible default).
231pub fn default_access_config() -> serde_json::Value {
232    serde_json::json!({ "authenticationMode": "CONFIG_MAP" })
233}
234
235/// The `upgradePolicy` a cluster reports when none was supplied at create time:
236/// `EXTENDED` support (extended support enabled by default).
237pub fn default_upgrade_policy() -> serde_json::Value {
238    serde_json::json!({ "supportType": "EXTENDED" })
239}
240
241/// An upgrade-readiness/misconfiguration Insight that EKS auto-generates for a
242/// cluster, a cluster sub-resource keyed by its generated id. Read back through
243/// `ListInsights` / `DescribeInsight`.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct Insight {
246    pub id: String,
247    pub name: String,
248    pub category: String,
249    pub kubernetes_version: String,
250    pub description: String,
251    pub recommendation: String,
252    /// The `InsightStatusValue` (PASSING / WARNING / ERROR / UNKNOWN).
253    pub status: String,
254    pub reason: String,
255    pub last_refresh_time: DateTime<Utc>,
256    pub last_transition_time: DateTime<Utc>,
257}
258
259/// The state of the most recent `StartInsightsRefresh` for a cluster, read back
260/// through `DescribeInsightsRefresh`.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct InsightsRefresh {
263    /// The `InsightsRefreshStatus` (IN_PROGRESS / FAILED / COMPLETED).
264    pub status: String,
265    pub started_at: DateTime<Utc>,
266    pub ended_at: Option<DateTime<Utc>>,
267}
268
269/// An EKS cluster capability (e.g. an ACK / KRO / ArgoCD controller), a
270/// sub-resource of a cluster keyed by `capabilityName`.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct Capability {
273    pub name: String,
274    pub arn: String,
275    pub cluster_name: String,
276    /// The `CapabilityType` (ACK / KRO / ARGOCD).
277    pub type_: String,
278    pub role_arn: String,
279    pub status: String,
280    pub version: String,
281    /// The `CapabilityConfigurationResponse`-shaped object, when supplied.
282    pub configuration: Option<serde_json::Value>,
283    pub tags: TagMap,
284    pub created_at: DateTime<Utc>,
285    pub modified_at: DateTime<Utc>,
286    pub delete_propagation_policy: Option<String>,
287}
288
289/// An EKS Anywhere subscription, an account-scoped (not cluster-scoped)
290/// resource keyed by its generated id.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct EksAnywhereSubscription {
293    pub id: String,
294    pub arn: String,
295    pub name: String,
296    pub created_at: DateTime<Utc>,
297    pub effective_date: DateTime<Utc>,
298    pub expiration_date: DateTime<Utc>,
299    pub license_quantity: i64,
300    /// The `EksAnywhereSubscriptionLicenseType` (only `Cluster` today).
301    pub license_type: String,
302    pub term_duration: i64,
303    /// The `EksAnywhereSubscriptionTermUnit` (only `MONTHS` today).
304    pub term_unit: String,
305    pub status: String,
306    pub auto_renew: bool,
307    pub tags: TagMap,
308}
309
310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
311pub struct EksState {
312    pub clusters: BTreeMap<String, Cluster>,
313    /// Tags keyed by resource ARN. Cluster tags live on the cluster itself;
314    /// this map holds tags applied to any other EKS ARN via `TagResource`.
315    pub tags: BTreeMap<String, TagMap>,
316    /// Node groups nested as cluster name -> nodegroup name -> node group.
317    /// Nesting (rather than a `(String, String)` tuple key) keeps the map
318    /// JSON-serializable through the snapshot and scopes node groups per
319    /// cluster for natural cross-cluster isolation.
320    #[serde(default)]
321    pub nodegroups: BTreeMap<String, BTreeMap<String, Nodegroup>>,
322    /// Fargate profiles nested as cluster name -> profile name -> profile.
323    #[serde(default)]
324    pub fargate_profiles: BTreeMap<String, BTreeMap<String, FargateProfile>>,
325    /// Add-ons nested as cluster name -> addon name -> addon.
326    #[serde(default)]
327    pub addons: BTreeMap<String, BTreeMap<String, Addon>>,
328    /// Access entries nested as cluster name -> principal ARN -> access entry.
329    #[serde(default)]
330    pub access_entries: BTreeMap<String, BTreeMap<String, AccessEntry>>,
331    /// Identity-provider configs nested as cluster name -> config name -> config.
332    #[serde(default)]
333    pub identity_provider_configs: BTreeMap<String, BTreeMap<String, IdentityProviderConfig>>,
334    /// Pod identity associations nested as cluster name -> associationId -> assoc.
335    #[serde(default)]
336    pub pod_identity_associations: BTreeMap<String, BTreeMap<String, PodIdentityAssociation>>,
337    /// Auto-generated Insights nested as cluster name -> insight id -> insight.
338    #[serde(default)]
339    pub insights: BTreeMap<String, BTreeMap<String, Insight>>,
340    /// The most recent insights-refresh state, keyed by cluster name.
341    #[serde(default)]
342    pub insights_refresh: BTreeMap<String, InsightsRefresh>,
343    /// Capabilities nested as cluster name -> capability name -> capability.
344    #[serde(default)]
345    pub capabilities: BTreeMap<String, BTreeMap<String, Capability>>,
346    /// EKS Anywhere subscriptions (account-scoped) keyed by subscription id.
347    #[serde(default)]
348    pub eks_anywhere_subscriptions: BTreeMap<String, EksAnywhereSubscription>,
349}
350
351impl AccountState for EksState {
352    fn new_for_account(_account_id: &str, _region: &str, _endpoint: &str) -> Self {
353        Self::default()
354    }
355}
356
357/// Build the ARN for a cluster.
358pub fn cluster_arn(region: &str, account_id: &str, name: &str) -> String {
359    format!("arn:aws:eks:{region}:{account_id}:cluster/{name}")
360}
361
362/// Build the ARN for a node group. AWS appends a random UUID segment after
363/// `nodegroup/{cluster}/{name}` so each node group's ARN is unique even after
364/// a delete/recreate.
365pub fn nodegroup_arn(
366    region: &str,
367    account_id: &str,
368    cluster: &str,
369    name: &str,
370    id: &str,
371) -> String {
372    format!("arn:aws:eks:{region}:{account_id}:nodegroup/{cluster}/{name}/{id}")
373}
374
375/// Build the ARN for a Fargate profile.
376pub fn fargate_profile_arn(
377    region: &str,
378    account_id: &str,
379    cluster: &str,
380    name: &str,
381    id: &str,
382) -> String {
383    format!("arn:aws:eks:{region}:{account_id}:fargateprofile/{cluster}/{name}/{id}")
384}
385
386/// Build the ARN for an add-on. AWS appends a random UUID segment after
387/// `addon/{cluster}/{name}` so each add-on's ARN is unique across recreate.
388pub fn addon_arn(region: &str, account_id: &str, cluster: &str, name: &str, id: &str) -> String {
389    format!("arn:aws:eks:{region}:{account_id}:addon/{cluster}/{name}/{id}")
390}
391
392/// Build the ARN for an access entry. AWS's access-entry ARN embeds the
393/// principal type (`role`/`user`), the account, the principal's resource name,
394/// and a random UUID:
395/// `arn:aws:eks:{region}:{account}:access-entry/{cluster}/{type}/{account}/{name}/{uuid}`.
396pub fn access_entry_arn(
397    region: &str,
398    account_id: &str,
399    cluster: &str,
400    principal_type: &str,
401    principal_name: &str,
402    id: &str,
403) -> String {
404    format!(
405        "arn:aws:eks:{region}:{account_id}:access-entry/{cluster}/{principal_type}/{account_id}/{principal_name}/{id}"
406    )
407}
408
409/// Build the ARN for an OIDC identity-provider config. AWS embeds the config
410/// type (`oidc`), the config name, and a random UUID.
411pub fn identity_provider_config_arn(
412    region: &str,
413    account_id: &str,
414    cluster: &str,
415    name: &str,
416    id: &str,
417) -> String {
418    format!("arn:aws:eks:{region}:{account_id}:identityproviderconfig/{cluster}/oidc/{name}/{id}")
419}
420
421/// Build the ARN for a pod identity association attached to an add-on.
422pub fn pod_identity_association_arn(
423    region: &str,
424    account_id: &str,
425    cluster: &str,
426    id: &str,
427) -> String {
428    format!("arn:aws:eks:{region}:{account_id}:podidentityassociation/{cluster}/a-{id}")
429}
430
431/// Build the ARN for a cluster capability. AWS appends a random UUID segment
432/// after `capability/{cluster}/{name}` so each capability's ARN is unique.
433pub fn capability_arn(
434    region: &str,
435    account_id: &str,
436    cluster: &str,
437    name: &str,
438    id: &str,
439) -> String {
440    format!("arn:aws:eks:{region}:{account_id}:capability/{cluster}/{name}/{id}")
441}
442
443/// Build the ARN for an EKS Anywhere subscription.
444pub fn eks_anywhere_subscription_arn(region: &str, account_id: &str, id: &str) -> String {
445    format!("arn:aws:eks:{region}:{account_id}:eks-anywhere-subscription/{id}")
446}
447
448pub type SharedEksState = Arc<RwLock<MultiAccountState<EksState>>>;
449
450#[derive(Debug, Serialize, Deserialize)]
451pub struct EksSnapshot {
452    pub schema_version: u32,
453    pub accounts: MultiAccountState<EksState>,
454}