Skip to main content

vantage_aws/models/
mod.rs

1//! Ready-made tables to skip the table-name dance.
2//!
3//! CloudWatch Logs (JSON-1.1, under [`logs`]):
4//!   - [`logs::groups_table`]  — `DescribeLogGroups`
5//!   - [`logs::streams_table`] — `DescribeLogStreams`
6//!   - [`logs::events_table`]  — `FilterLogEvents`
7//!
8//! ECS (JSON-1.1, under [`ecs`]):
9//!   - [`ecs::clusters_table`]
10//!   - [`ecs::services_table`]
11//!   - [`ecs::tasks_table`]
12//!   - [`ecs::task_definitions_table`]
13//!
14//! IAM (Query, under [`iam`]):
15//!   - [`iam::users_table`]              — `ListUsers`
16//!   - [`iam::groups_table`]             — `ListGroups`
17//!   - [`iam::roles_table`]              — `ListRoles`
18//!   - [`iam::policies_table`]           — `ListPolicies`
19//!   - [`iam::access_keys_table`]        — `ListAccessKeys`  (per user)
20//!   - [`iam::instance_profiles_table`]  — `ListInstanceProfiles`
21//!
22//! ## Generic factory ([`Factory`])
23//!
24//! Wraps every table above behind dotted-string names (`iam.users`,
25//! `log.group`, `ecs.task_definitions`, …) and a single
26//! [`Factory::from_arn`] entry point. Powers the `aws-cli` example
27//! (which adapts it to `vantage_cli_util`'s `ModelFactory` trait);
28//! anything else that needs a generic, type-erased AWS table by name
29//! can reuse it without dragging in a CLI rendering crate.
30//!
31//! ```no_run
32//! # use vantage_aws::{AwsAccount, eq};
33//! # use vantage_aws::models::logs::groups_table;
34//! # async fn run() -> vantage_core::Result<()> {
35//! let aws = AwsAccount::from_default()?;
36//! let mut groups = groups_table(aws);
37//! groups.add_condition(eq("logGroupNamePrefix", "/aws/lambda/"));
38//! # Ok(()) }
39//! ```
40
41pub mod ecs;
42pub mod iam;
43pub mod logs;
44
45use vantage_table::any::AnyTable;
46
47use crate::AwsAccount;
48
49/// Whether a [`Factory`] lookup should drop into list mode (returning
50/// every matching record) or single-record mode (returning just the
51/// first match).
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum FactoryMode {
54    List,
55    Single,
56}
57
58/// Generic, type-erased model factory.
59///
60/// The factory maps dotted string names to the typed `*_table`
61/// factories above and dispatches ARN parsing across each entity's
62/// `from_arn`. Singular forms (e.g. `iam.user`) drop into
63/// [`FactoryMode::Single`]; plural forms (`iam.users`) drop into
64/// [`FactoryMode::List`].
65#[derive(Debug, Clone)]
66pub struct Factory {
67    aws: AwsAccount,
68}
69
70impl Factory {
71    /// Build a factory bound to a specific AWS account.
72    pub fn new(aws: AwsAccount) -> Self {
73        Self { aws }
74    }
75
76    /// All known model names, in registration order.
77    ///
78    /// Models whose AWS API requires a parent filter aren't exposed
79    /// top-level — listing them standalone would either error
80    /// or quietly return only the caller's slice. Reach them via
81    /// traversal from their parent:
82    ///   - `iam.user ... :access_keys`        (ListAccessKeys needs UserName)
83    ///   - `log.group ... :streams`           (DescribeLogStreams needs logGroupName)
84    ///   - `log.group ... :events`            (FilterLogEvents needs logGroupName)
85    ///   - `ecs.cluster ... :services`        (ListServices needs cluster)
86    ///   - `ecs.cluster ... :tasks`           (ListTasks needs cluster)
87    ///
88    /// Per-resource ARNs still work as the first argument for any of
89    /// these — see [`Factory::from_arn`].
90    pub fn known_names() -> &'static [&'static str] {
91        &[
92            "iam.user",
93            "iam.users",
94            "iam.group",
95            "iam.groups",
96            "iam.role",
97            "iam.roles",
98            "iam.policy",
99            "iam.policies",
100            "iam.instance_profile",
101            "iam.instance_profiles",
102            "log.group",
103            "log.groups",
104            "ecs.cluster",
105            "ecs.clusters",
106            "ecs.task_definition",
107            "ecs.task_definitions",
108        ]
109    }
110
111    /// Resolve a model name to an `AnyTable` plus its mode.
112    pub fn for_name(&self, name: &str) -> Option<(AnyTable, FactoryMode)> {
113        let aws = self.aws.clone();
114        let (table, mode) = match name {
115            "iam.user" => (AnyTable::new(iam::users_table(aws)), FactoryMode::Single),
116            "iam.users" => (AnyTable::new(iam::users_table(aws)), FactoryMode::List),
117            "iam.group" => (AnyTable::new(iam::groups_table(aws)), FactoryMode::Single),
118            "iam.groups" => (AnyTable::new(iam::groups_table(aws)), FactoryMode::List),
119            "iam.role" => (AnyTable::new(iam::roles_table(aws)), FactoryMode::Single),
120            "iam.roles" => (AnyTable::new(iam::roles_table(aws)), FactoryMode::List),
121            "iam.policy" => (AnyTable::new(iam::policies_table(aws)), FactoryMode::Single),
122            "iam.policies" => (AnyTable::new(iam::policies_table(aws)), FactoryMode::List),
123            // iam.access_key / iam.access_keys intentionally omitted:
124            // listing them standalone returns just the caller's keys,
125            // which is rarely what people mean. Reach them via
126            // `iam.user ... :access_keys`.
127            "iam.instance_profile" => (
128                AnyTable::new(iam::instance_profiles_table(aws)),
129                FactoryMode::Single,
130            ),
131            "iam.instance_profiles" => (
132                AnyTable::new(iam::instance_profiles_table(aws)),
133                FactoryMode::List,
134            ),
135            "log.group" => (AnyTable::new(logs::groups_table(aws)), FactoryMode::Single),
136            "log.groups" => (AnyTable::new(logs::groups_table(aws)), FactoryMode::List),
137            // log.stream / log.event intentionally omitted: AWS
138            // requires `logGroupName`. Reach them via
139            // `log.group ... :streams` / `:events`.
140            "ecs.cluster" => (AnyTable::new(ecs::clusters_table(aws)), FactoryMode::Single),
141            "ecs.clusters" => (AnyTable::new(ecs::clusters_table(aws)), FactoryMode::List),
142            // ecs.service / ecs.task intentionally omitted: AWS
143            // requires `cluster` as a filter, so listing them
144            // standalone returns nothing useful. Reach them via
145            // `ecs.cluster ... :services` / `:tasks`.
146            "ecs.task_definition" => (
147                AnyTable::new(ecs::task_definitions_table(aws)),
148                FactoryMode::Single,
149            ),
150            "ecs.task_definitions" => (
151                AnyTable::new(ecs::task_definitions_table(aws)),
152                FactoryMode::List,
153            ),
154            _ => return None,
155        };
156        Some((table, mode))
157    }
158
159    /// Resolve an ARN to a pre-conditioned single-record table by
160    /// dispatching to each entity's `from_arn`. Returns `None` if no
161    /// entity recognises the ARN's resource type.
162    pub fn from_arn(&self, arn: &str) -> Option<AnyTable> {
163        let aws = self.aws.clone();
164        if let Some(t) = iam::user::User::from_arn(arn, aws.clone()) {
165            return Some(AnyTable::new(t));
166        }
167        if let Some(t) = iam::group::Group::from_arn(arn, aws.clone()) {
168            return Some(AnyTable::new(t));
169        }
170        if let Some(t) = iam::role::Role::from_arn(arn, aws.clone()) {
171            return Some(AnyTable::new(t));
172        }
173        if let Some(t) = iam::policy::Policy::from_arn(arn, aws.clone()) {
174            return Some(AnyTable::new(t));
175        }
176        if let Some(t) = iam::instance_profile::InstanceProfile::from_arn(arn, aws.clone()) {
177            return Some(AnyTable::new(t));
178        }
179        if let Some(t) = iam::access_key::AccessKey::from_arn(arn, aws.clone()) {
180            return Some(AnyTable::new(t));
181        }
182        if let Some(t) = logs::stream::LogStream::from_arn(arn, aws.clone()) {
183            return Some(AnyTable::new(t));
184        }
185        if let Some(t) = logs::group::LogGroup::from_arn(arn, aws.clone()) {
186            return Some(AnyTable::new(t));
187        }
188        if let Some(t) = ecs::cluster::Cluster::from_arn(arn, aws.clone()) {
189            return Some(AnyTable::new(t));
190        }
191        None
192    }
193}