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}