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#[cfg(feature = "vista")]
62use vantage_vista::Vista;
63
64use crate::AwsAccount;
65
66/// Whether a [`Factory`] lookup should drop into list mode (returning
67/// every matching record) or single-record mode (returning just the
68/// first match).
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum FactoryMode {
71    List,
72    Single,
73}
74
75/// Generic, type-erased model factory.
76///
77/// The factory maps dotted string names to the typed `*_table`
78/// factories above and dispatches ARN parsing across each entity's
79/// `from_arn`. Singular forms (e.g. `iam.user`) drop into
80/// [`FactoryMode::Single`]; plural forms (`iam.users`) drop into
81/// [`FactoryMode::List`].
82#[derive(Debug, Clone)]
83pub struct Factory {
84    aws: AwsAccount,
85}
86
87impl Factory {
88    /// Build a factory bound to a specific AWS account.
89    pub fn new(aws: AwsAccount) -> Self {
90        Self { aws }
91    }
92
93    /// All known model names, in registration order.
94    ///
95    /// Models whose AWS API requires a parent filter aren't exposed
96    /// top-level — listing them standalone would either error
97    /// or quietly return only the caller's slice. Reach them via
98    /// traversal from their parent:
99    ///   - `iam.user ... :access_keys`        (ListAccessKeys needs UserName)
100    ///   - `log.group ... :streams`           (DescribeLogStreams needs logGroupName)
101    ///   - `log.group ... :events`            (FilterLogEvents needs logGroupName)
102    ///   - `ecs.cluster ... :services`        (ListServices needs cluster)
103    ///   - `ecs.cluster ... :tasks`           (ListTasks needs cluster)
104    ///   - `s3.bucket ... :objects`           (ListObjectsV2 needs Bucket)
105    ///   - `lambda.function ... :aliases`     (ListAliases needs FunctionName)
106    ///   - `lambda.function ... :versions`    (ListVersionsByFunction needs FunctionName)
107    ///   - `lambda.function ... :log_group`   (CloudWatch group at /aws/lambda/<name>)
108    ///
109    /// Per-resource ARNs still work as the first argument for any of
110    /// these — see [`Factory::from_arn`].
111    pub fn known_names() -> &'static [&'static str] {
112        &[
113            "iam.user",
114            "iam.users",
115            "iam.group",
116            "iam.groups",
117            "iam.role",
118            "iam.roles",
119            "iam.policy",
120            "iam.policies",
121            "iam.instance_profile",
122            "iam.instance_profiles",
123            "log.group",
124            "log.groups",
125            "ecs.cluster",
126            "ecs.clusters",
127            "ecs.task_definition",
128            "ecs.task_definitions",
129            "s3.bucket",
130            "s3.buckets",
131            "lambda.function",
132            "lambda.functions",
133            "dynamodb.table",
134            "dynamodb.tables",
135        ]
136    }
137
138    /// Resolve a model name to an `AnyTable` plus its mode.
139    pub fn for_name(&self, name: &str) -> Option<(AnyTable, FactoryMode)> {
140        let aws = self.aws.clone();
141        let (table, mode) = match name {
142            "iam.user" => (AnyTable::new(iam::users_table(aws)), FactoryMode::Single),
143            "iam.users" => (AnyTable::new(iam::users_table(aws)), FactoryMode::List),
144            "iam.group" => (AnyTable::new(iam::groups_table(aws)), FactoryMode::Single),
145            "iam.groups" => (AnyTable::new(iam::groups_table(aws)), FactoryMode::List),
146            "iam.role" => (AnyTable::new(iam::roles_table(aws)), FactoryMode::Single),
147            "iam.roles" => (AnyTable::new(iam::roles_table(aws)), FactoryMode::List),
148            "iam.policy" => (AnyTable::new(iam::policies_table(aws)), FactoryMode::Single),
149            "iam.policies" => (AnyTable::new(iam::policies_table(aws)), FactoryMode::List),
150            // iam.access_key / iam.access_keys intentionally omitted:
151            // listing them standalone returns just the caller's keys,
152            // which is rarely what people mean. Reach them via
153            // `iam.user ... :access_keys`.
154            "iam.instance_profile" => (
155                AnyTable::new(iam::instance_profiles_table(aws)),
156                FactoryMode::Single,
157            ),
158            "iam.instance_profiles" => (
159                AnyTable::new(iam::instance_profiles_table(aws)),
160                FactoryMode::List,
161            ),
162            "log.group" => (AnyTable::new(logs::groups_table(aws)), FactoryMode::Single),
163            "log.groups" => (AnyTable::new(logs::groups_table(aws)), FactoryMode::List),
164            // log.stream / log.event intentionally omitted: AWS
165            // requires `logGroupName`. Reach them via
166            // `log.group ... :streams` / `:events`.
167            "ecs.cluster" => (AnyTable::new(ecs::clusters_table(aws)), FactoryMode::Single),
168            "ecs.clusters" => (AnyTable::new(ecs::clusters_table(aws)), FactoryMode::List),
169            // ecs.service / ecs.task intentionally omitted: AWS
170            // requires `cluster` as a filter, so listing them
171            // standalone returns nothing useful. Reach them via
172            // `ecs.cluster ... :services` / `:tasks`.
173            "ecs.task_definition" => (
174                AnyTable::new(ecs::task_definitions_table(aws)),
175                FactoryMode::Single,
176            ),
177            "ecs.task_definitions" => (
178                AnyTable::new(ecs::task_definitions_table(aws)),
179                FactoryMode::List,
180            ),
181            "s3.bucket" => (AnyTable::new(s3::buckets_table(aws)), FactoryMode::Single),
182            "s3.buckets" => (AnyTable::new(s3::buckets_table(aws)), FactoryMode::List),
183            // s3.object intentionally omitted: ListObjectsV2 requires
184            // a Bucket. Reach via `s3.bucket ... :objects`.
185            "lambda.function" => (
186                AnyTable::new(lambda::functions_table(aws)),
187                FactoryMode::Single,
188            ),
189            "lambda.functions" => (
190                AnyTable::new(lambda::functions_table(aws)),
191                FactoryMode::List,
192            ),
193            // lambda.alias / lambda.version intentionally omitted:
194            // both list APIs require FunctionName. Reach via
195            // `lambda.function ... :aliases` / `:versions`.
196            "dynamodb.table" => (
197                AnyTable::new(dynamodb::tables_table(aws)),
198                FactoryMode::Single,
199            ),
200            "dynamodb.tables" => (
201                AnyTable::new(dynamodb::tables_table(aws)),
202                FactoryMode::List,
203            ),
204            _ => return None,
205        };
206        Some((table, mode))
207    }
208
209    /// Resolve an ARN to a pre-conditioned single-record table by
210    /// dispatching to each entity's `from_arn`. Returns `None` if no
211    /// entity recognises the ARN's resource type.
212    pub fn from_arn(&self, arn: &str) -> Option<AnyTable> {
213        let aws = self.aws.clone();
214        if let Some(t) = iam::user::User::from_arn(arn, aws.clone()) {
215            return Some(AnyTable::new(t));
216        }
217        if let Some(t) = iam::group::Group::from_arn(arn, aws.clone()) {
218            return Some(AnyTable::new(t));
219        }
220        if let Some(t) = iam::role::Role::from_arn(arn, aws.clone()) {
221            return Some(AnyTable::new(t));
222        }
223        if let Some(t) = iam::policy::Policy::from_arn(arn, aws.clone()) {
224            return Some(AnyTable::new(t));
225        }
226        if let Some(t) = iam::instance_profile::InstanceProfile::from_arn(arn, aws.clone()) {
227            return Some(AnyTable::new(t));
228        }
229        if let Some(t) = iam::access_key::AccessKey::from_arn(arn, aws.clone()) {
230            return Some(AnyTable::new(t));
231        }
232        if let Some(t) = logs::stream::LogStream::from_arn(arn, aws.clone()) {
233            return Some(AnyTable::new(t));
234        }
235        if let Some(t) = logs::group::LogGroup::from_arn(arn, aws.clone()) {
236            return Some(AnyTable::new(t));
237        }
238        if let Some(t) = ecs::cluster::Cluster::from_arn(arn, aws.clone()) {
239            return Some(AnyTable::new(t));
240        }
241        // S3 — object ARNs (`arn:aws:s3:::bucket/key`) check first
242        // since they're a strict superset of bucket ARNs.
243        if let Some(t) = s3::object::Object::from_arn(arn, aws.clone()) {
244            return Some(AnyTable::new(t));
245        }
246        if let Some(t) = s3::bucket::Bucket::from_arn(arn, aws.clone()) {
247            return Some(AnyTable::new(t));
248        }
249        if let Some(t) = lambda::function::Function::from_arn(arn, aws.clone()) {
250            return Some(AnyTable::new(t));
251        }
252        if let Some(t) = dynamodb::table::DynamoDbTable::from_arn(arn, aws.clone()) {
253            return Some(AnyTable::new(t));
254        }
255        None
256    }
257
258    /// Vista-flavoured mirror of [`Self::for_name`]. Returns a fully
259    /// constructed [`Vista`] (carrying the same per-table metadata as
260    /// `AnyTable`) plus the model's natural mode.
261    ///
262    /// Composite-id endpoints (`iam.access_keys`, `s3.objects`,
263    /// `lambda.aliases`, `lambda.versions`, `ecs.services`, `ecs.tasks`,
264    /// `log.streams`, `log.events`) intentionally aren't surfaced here for
265    /// the same reason they're absent from [`Self::for_name`]: the AWS
266    /// list endpoint requires a parent filter, so the only useful way to
267    /// reach them is via `:relation` traversal from the parent.
268    #[cfg(feature = "vista")]
269    pub fn vista_for_name(&self, name: &str) -> Option<(Vista, FactoryMode)> {
270        let aws = self.aws.clone();
271        let factory = aws.vista_factory();
272        let (vista, mode) = match name {
273            "iam.user" => (
274                factory.from_table(iam::users_table(aws)).ok()?,
275                FactoryMode::Single,
276            ),
277            "iam.users" => (
278                factory.from_table(iam::users_table(aws)).ok()?,
279                FactoryMode::List,
280            ),
281            "iam.group" => (
282                factory.from_table(iam::groups_table(aws)).ok()?,
283                FactoryMode::Single,
284            ),
285            "iam.groups" => (
286                factory.from_table(iam::groups_table(aws)).ok()?,
287                FactoryMode::List,
288            ),
289            "iam.role" => (
290                factory.from_table(iam::roles_table(aws)).ok()?,
291                FactoryMode::Single,
292            ),
293            "iam.roles" => (
294                factory.from_table(iam::roles_table(aws)).ok()?,
295                FactoryMode::List,
296            ),
297            "iam.policy" => (
298                factory.from_table(iam::policies_table(aws)).ok()?,
299                FactoryMode::Single,
300            ),
301            "iam.policies" => (
302                factory.from_table(iam::policies_table(aws)).ok()?,
303                FactoryMode::List,
304            ),
305            "iam.instance_profile" => (
306                factory.from_table(iam::instance_profiles_table(aws)).ok()?,
307                FactoryMode::Single,
308            ),
309            "iam.instance_profiles" => (
310                factory.from_table(iam::instance_profiles_table(aws)).ok()?,
311                FactoryMode::List,
312            ),
313            "log.group" => (
314                factory.from_table(logs::groups_table(aws)).ok()?,
315                FactoryMode::Single,
316            ),
317            "log.groups" => (
318                factory.from_table(logs::groups_table(aws)).ok()?,
319                FactoryMode::List,
320            ),
321            "ecs.cluster" => (
322                factory.from_table(ecs::clusters_table(aws)).ok()?,
323                FactoryMode::Single,
324            ),
325            "ecs.clusters" => (
326                factory.from_table(ecs::clusters_table(aws)).ok()?,
327                FactoryMode::List,
328            ),
329            "ecs.task_definition" => (
330                factory.from_table(ecs::task_definitions_table(aws)).ok()?,
331                FactoryMode::Single,
332            ),
333            "ecs.task_definitions" => (
334                factory.from_table(ecs::task_definitions_table(aws)).ok()?,
335                FactoryMode::List,
336            ),
337            "s3.bucket" => (
338                factory.from_table(s3::buckets_table(aws)).ok()?,
339                FactoryMode::Single,
340            ),
341            "s3.buckets" => (
342                factory.from_table(s3::buckets_table(aws)).ok()?,
343                FactoryMode::List,
344            ),
345            "lambda.function" => (
346                factory.from_table(lambda::functions_table(aws)).ok()?,
347                FactoryMode::Single,
348            ),
349            "lambda.functions" => (
350                factory.from_table(lambda::functions_table(aws)).ok()?,
351                FactoryMode::List,
352            ),
353            "dynamodb.table" => (
354                factory.from_table(dynamodb::tables_table(aws)).ok()?,
355                FactoryMode::Single,
356            ),
357            "dynamodb.tables" => (
358                factory.from_table(dynamodb::tables_table(aws)).ok()?,
359                FactoryMode::List,
360            ),
361            _ => return None,
362        };
363        Some((vista, mode))
364    }
365
366    /// Vista-flavoured mirror of [`Self::from_arn`]. Dispatch order
367    /// matches `from_arn` exactly — S3 objects probed before buckets,
368    /// etc.
369    #[cfg(feature = "vista")]
370    pub fn vista_from_arn(&self, arn: &str) -> Option<Vista> {
371        let aws = self.aws.clone();
372        let factory = aws.vista_factory();
373        if let Some(t) = iam::user::User::from_arn(arn, aws.clone()) {
374            return factory.from_table(t).ok();
375        }
376        if let Some(t) = iam::group::Group::from_arn(arn, aws.clone()) {
377            return factory.from_table(t).ok();
378        }
379        if let Some(t) = iam::role::Role::from_arn(arn, aws.clone()) {
380            return factory.from_table(t).ok();
381        }
382        if let Some(t) = iam::policy::Policy::from_arn(arn, aws.clone()) {
383            return factory.from_table(t).ok();
384        }
385        if let Some(t) = iam::instance_profile::InstanceProfile::from_arn(arn, aws.clone()) {
386            return factory.from_table(t).ok();
387        }
388        if let Some(t) = iam::access_key::AccessKey::from_arn(arn, aws.clone()) {
389            return factory.from_table(t).ok();
390        }
391        if let Some(t) = logs::stream::LogStream::from_arn(arn, aws.clone()) {
392            return factory.from_table(t).ok();
393        }
394        if let Some(t) = logs::group::LogGroup::from_arn(arn, aws.clone()) {
395            return factory.from_table(t).ok();
396        }
397        if let Some(t) = ecs::cluster::Cluster::from_arn(arn, aws.clone()) {
398            return factory.from_table(t).ok();
399        }
400        if let Some(t) = s3::object::Object::from_arn(arn, aws.clone()) {
401            return factory.from_table(t).ok();
402        }
403        if let Some(t) = s3::bucket::Bucket::from_arn(arn, aws.clone()) {
404            return factory.from_table(t).ok();
405        }
406        if let Some(t) = lambda::function::Function::from_arn(arn, aws.clone()) {
407            return factory.from_table(t).ok();
408        }
409        if let Some(t) = dynamodb::table::DynamoDbTable::from_arn(arn, aws.clone()) {
410            return factory.from_table(t).ok();
411        }
412        None
413    }
414}