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//! S3 (REST-XML, under [`s3`]):
23//!   - [`s3::buckets_table`] — `ListBuckets`
24//!   - [`s3::objects_table`] — `ListObjectsV2` (per bucket)
25//!
26//! Lambda (REST-JSON, under [`lambda`]):
27//!   - [`lambda::functions_table`] — `ListFunctions`
28//!   - [`lambda::aliases_table`]   — `ListAliases` (per function)
29//!   - [`lambda::versions_table`]  — `ListVersionsByFunction` (per function)
30//!
31//! DynamoDB (JSON-1.0, under [`dynamodb`]):
32//!   - [`dynamodb::tables_table`] — `ListTables`
33//!
34//! ## Generic factory ([`Factory`])
35//!
36//! Wraps every table above behind dotted-string names (`iam.users`,
37//! `log.group`, `ecs.task_definitions`, …) and a single
38//! [`Factory::from_arn`] entry point. Powers the `aws-cli` example
39//! (which adapts it to `vantage_cli_util`'s `ModelFactory` trait);
40//! anything else that needs a generic, type-erased AWS table by name
41//! can reuse it without dragging in a CLI rendering crate.
42//!
43//! ```no_run
44//! # use vantage_aws::{AwsAccount, eq};
45//! # use vantage_aws::models::logs::groups_table;
46//! # async fn run() -> vantage_core::Result<()> {
47//! let aws = AwsAccount::from_default()?;
48//! let mut groups = groups_table(aws);
49//! groups.add_condition(eq("logGroupNamePrefix", "/aws/lambda/"));
50//! # Ok(()) }
51//! ```
52
53pub mod dynamodb;
54pub mod ecs;
55pub mod iam;
56pub mod lambda;
57pub mod logs;
58pub mod s3;
59
60use vantage_table::any::AnyTable;
61
62use crate::AwsAccount;
63
64/// Whether a [`Factory`] lookup should drop into list mode (returning
65/// every matching record) or single-record mode (returning just the
66/// first match).
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum FactoryMode {
69    List,
70    Single,
71}
72
73/// Generic, type-erased model factory.
74///
75/// The factory maps dotted string names to the typed `*_table`
76/// factories above and dispatches ARN parsing across each entity's
77/// `from_arn`. Singular forms (e.g. `iam.user`) drop into
78/// [`FactoryMode::Single`]; plural forms (`iam.users`) drop into
79/// [`FactoryMode::List`].
80#[derive(Debug, Clone)]
81pub struct Factory {
82    aws: AwsAccount,
83}
84
85impl Factory {
86    /// Build a factory bound to a specific AWS account.
87    pub fn new(aws: AwsAccount) -> Self {
88        Self { aws }
89    }
90
91    /// All known model names, in registration order.
92    ///
93    /// Models whose AWS API requires a parent filter aren't exposed
94    /// top-level — listing them standalone would either error
95    /// or quietly return only the caller's slice. Reach them via
96    /// traversal from their parent:
97    ///   - `iam.user ... :access_keys`        (ListAccessKeys needs UserName)
98    ///   - `log.group ... :streams`           (DescribeLogStreams needs logGroupName)
99    ///   - `log.group ... :events`            (FilterLogEvents needs logGroupName)
100    ///   - `ecs.cluster ... :services`        (ListServices needs cluster)
101    ///   - `ecs.cluster ... :tasks`           (ListTasks needs cluster)
102    ///   - `s3.bucket ... :objects`           (ListObjectsV2 needs Bucket)
103    ///   - `lambda.function ... :aliases`     (ListAliases needs FunctionName)
104    ///   - `lambda.function ... :versions`    (ListVersionsByFunction needs FunctionName)
105    ///   - `lambda.function ... :log_group`   (CloudWatch group at /aws/lambda/<name>)
106    ///
107    /// Per-resource ARNs still work as the first argument for any of
108    /// these — see [`Factory::from_arn`].
109    pub fn known_names() -> &'static [&'static str] {
110        &[
111            "iam.user",
112            "iam.users",
113            "iam.group",
114            "iam.groups",
115            "iam.role",
116            "iam.roles",
117            "iam.policy",
118            "iam.policies",
119            "iam.instance_profile",
120            "iam.instance_profiles",
121            "log.group",
122            "log.groups",
123            "ecs.cluster",
124            "ecs.clusters",
125            "ecs.task_definition",
126            "ecs.task_definitions",
127            "s3.bucket",
128            "s3.buckets",
129            "lambda.function",
130            "lambda.functions",
131            "dynamodb.table",
132            "dynamodb.tables",
133        ]
134    }
135
136    /// Resolve a model name to an `AnyTable` plus its mode.
137    pub fn for_name(&self, name: &str) -> Option<(AnyTable, FactoryMode)> {
138        let aws = self.aws.clone();
139        let (table, mode) = match name {
140            "iam.user" => (AnyTable::new(iam::users_table(aws)), FactoryMode::Single),
141            "iam.users" => (AnyTable::new(iam::users_table(aws)), FactoryMode::List),
142            "iam.group" => (AnyTable::new(iam::groups_table(aws)), FactoryMode::Single),
143            "iam.groups" => (AnyTable::new(iam::groups_table(aws)), FactoryMode::List),
144            "iam.role" => (AnyTable::new(iam::roles_table(aws)), FactoryMode::Single),
145            "iam.roles" => (AnyTable::new(iam::roles_table(aws)), FactoryMode::List),
146            "iam.policy" => (AnyTable::new(iam::policies_table(aws)), FactoryMode::Single),
147            "iam.policies" => (AnyTable::new(iam::policies_table(aws)), FactoryMode::List),
148            // iam.access_key / iam.access_keys intentionally omitted:
149            // listing them standalone returns just the caller's keys,
150            // which is rarely what people mean. Reach them via
151            // `iam.user ... :access_keys`.
152            "iam.instance_profile" => (
153                AnyTable::new(iam::instance_profiles_table(aws)),
154                FactoryMode::Single,
155            ),
156            "iam.instance_profiles" => (
157                AnyTable::new(iam::instance_profiles_table(aws)),
158                FactoryMode::List,
159            ),
160            "log.group" => (AnyTable::new(logs::groups_table(aws)), FactoryMode::Single),
161            "log.groups" => (AnyTable::new(logs::groups_table(aws)), FactoryMode::List),
162            // log.stream / log.event intentionally omitted: AWS
163            // requires `logGroupName`. Reach them via
164            // `log.group ... :streams` / `:events`.
165            "ecs.cluster" => (AnyTable::new(ecs::clusters_table(aws)), FactoryMode::Single),
166            "ecs.clusters" => (AnyTable::new(ecs::clusters_table(aws)), FactoryMode::List),
167            // ecs.service / ecs.task intentionally omitted: AWS
168            // requires `cluster` as a filter, so listing them
169            // standalone returns nothing useful. Reach them via
170            // `ecs.cluster ... :services` / `:tasks`.
171            "ecs.task_definition" => (
172                AnyTable::new(ecs::task_definitions_table(aws)),
173                FactoryMode::Single,
174            ),
175            "ecs.task_definitions" => (
176                AnyTable::new(ecs::task_definitions_table(aws)),
177                FactoryMode::List,
178            ),
179            "s3.bucket" => (AnyTable::new(s3::buckets_table(aws)), FactoryMode::Single),
180            "s3.buckets" => (AnyTable::new(s3::buckets_table(aws)), FactoryMode::List),
181            // s3.object intentionally omitted: ListObjectsV2 requires
182            // a Bucket. Reach via `s3.bucket ... :objects`.
183            "lambda.function" => (
184                AnyTable::new(lambda::functions_table(aws)),
185                FactoryMode::Single,
186            ),
187            "lambda.functions" => (
188                AnyTable::new(lambda::functions_table(aws)),
189                FactoryMode::List,
190            ),
191            // lambda.alias / lambda.version intentionally omitted:
192            // both list APIs require FunctionName. Reach via
193            // `lambda.function ... :aliases` / `:versions`.
194            "dynamodb.table" => (
195                AnyTable::new(dynamodb::tables_table(aws)),
196                FactoryMode::Single,
197            ),
198            "dynamodb.tables" => (
199                AnyTable::new(dynamodb::tables_table(aws)),
200                FactoryMode::List,
201            ),
202            _ => return None,
203        };
204        Some((table, mode))
205    }
206
207    /// Resolve an ARN to a pre-conditioned single-record table by
208    /// dispatching to each entity's `from_arn`. Returns `None` if no
209    /// entity recognises the ARN's resource type.
210    pub fn from_arn(&self, arn: &str) -> Option<AnyTable> {
211        let aws = self.aws.clone();
212        if let Some(t) = iam::user::User::from_arn(arn, aws.clone()) {
213            return Some(AnyTable::new(t));
214        }
215        if let Some(t) = iam::group::Group::from_arn(arn, aws.clone()) {
216            return Some(AnyTable::new(t));
217        }
218        if let Some(t) = iam::role::Role::from_arn(arn, aws.clone()) {
219            return Some(AnyTable::new(t));
220        }
221        if let Some(t) = iam::policy::Policy::from_arn(arn, aws.clone()) {
222            return Some(AnyTable::new(t));
223        }
224        if let Some(t) = iam::instance_profile::InstanceProfile::from_arn(arn, aws.clone()) {
225            return Some(AnyTable::new(t));
226        }
227        if let Some(t) = iam::access_key::AccessKey::from_arn(arn, aws.clone()) {
228            return Some(AnyTable::new(t));
229        }
230        if let Some(t) = logs::stream::LogStream::from_arn(arn, aws.clone()) {
231            return Some(AnyTable::new(t));
232        }
233        if let Some(t) = logs::group::LogGroup::from_arn(arn, aws.clone()) {
234            return Some(AnyTable::new(t));
235        }
236        if let Some(t) = ecs::cluster::Cluster::from_arn(arn, aws.clone()) {
237            return Some(AnyTable::new(t));
238        }
239        // S3 — object ARNs (`arn:aws:s3:::bucket/key`) check first
240        // since they're a strict superset of bucket ARNs.
241        if let Some(t) = s3::object::Object::from_arn(arn, aws.clone()) {
242            return Some(AnyTable::new(t));
243        }
244        if let Some(t) = s3::bucket::Bucket::from_arn(arn, aws.clone()) {
245            return Some(AnyTable::new(t));
246        }
247        if let Some(t) = lambda::function::Function::from_arn(arn, aws.clone()) {
248            return Some(AnyTable::new(t));
249        }
250        if let Some(t) = dynamodb::table::DynamoDbTable::from_arn(arn, aws.clone()) {
251            return Some(AnyTable::new(t));
252        }
253        None
254    }
255}