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