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}