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_vista::Vista;
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 a fully-constructed [`Vista`] plus its
137 /// natural mode (singular → [`FactoryMode::Single`], plural →
138 /// [`FactoryMode::List`]).
139 ///
140 /// Composite-id endpoints (`iam.access_keys`, `s3.objects`,
141 /// `lambda.aliases`, `lambda.versions`, `ecs.services`, `ecs.tasks`,
142 /// `log.streams`, `log.events`) intentionally aren't surfaced here:
143 /// the AWS list endpoint requires a parent filter, so the only
144 /// useful way to reach them is via `:relation` traversal from the
145 /// parent.
146 pub fn for_name(&self, name: &str) -> Option<(Vista, FactoryMode)> {
147 let aws = self.aws.clone();
148 let factory = aws.vista_factory();
149 let (vista, mode) = match name {
150 "iam.user" => (
151 factory.from_table(iam::users_table(aws)).ok()?,
152 FactoryMode::Single,
153 ),
154 "iam.users" => (
155 factory.from_table(iam::users_table(aws)).ok()?,
156 FactoryMode::List,
157 ),
158 "iam.group" => (
159 factory.from_table(iam::groups_table(aws)).ok()?,
160 FactoryMode::Single,
161 ),
162 "iam.groups" => (
163 factory.from_table(iam::groups_table(aws)).ok()?,
164 FactoryMode::List,
165 ),
166 "iam.role" => (
167 factory.from_table(iam::roles_table(aws)).ok()?,
168 FactoryMode::Single,
169 ),
170 "iam.roles" => (
171 factory.from_table(iam::roles_table(aws)).ok()?,
172 FactoryMode::List,
173 ),
174 "iam.policy" => (
175 factory.from_table(iam::policies_table(aws)).ok()?,
176 FactoryMode::Single,
177 ),
178 "iam.policies" => (
179 factory.from_table(iam::policies_table(aws)).ok()?,
180 FactoryMode::List,
181 ),
182 "iam.instance_profile" => (
183 factory.from_table(iam::instance_profiles_table(aws)).ok()?,
184 FactoryMode::Single,
185 ),
186 "iam.instance_profiles" => (
187 factory.from_table(iam::instance_profiles_table(aws)).ok()?,
188 FactoryMode::List,
189 ),
190 "log.group" => (
191 factory.from_table(logs::groups_table(aws)).ok()?,
192 FactoryMode::Single,
193 ),
194 "log.groups" => (
195 factory.from_table(logs::groups_table(aws)).ok()?,
196 FactoryMode::List,
197 ),
198 "ecs.cluster" => (
199 factory.from_table(ecs::clusters_table(aws)).ok()?,
200 FactoryMode::Single,
201 ),
202 "ecs.clusters" => (
203 factory.from_table(ecs::clusters_table(aws)).ok()?,
204 FactoryMode::List,
205 ),
206 "ecs.task_definition" => (
207 factory.from_table(ecs::task_definitions_table(aws)).ok()?,
208 FactoryMode::Single,
209 ),
210 "ecs.task_definitions" => (
211 factory.from_table(ecs::task_definitions_table(aws)).ok()?,
212 FactoryMode::List,
213 ),
214 "s3.bucket" => (
215 factory.from_table(s3::buckets_table(aws)).ok()?,
216 FactoryMode::Single,
217 ),
218 "s3.buckets" => (
219 factory.from_table(s3::buckets_table(aws)).ok()?,
220 FactoryMode::List,
221 ),
222 "lambda.function" => (
223 factory.from_table(lambda::functions_table(aws)).ok()?,
224 FactoryMode::Single,
225 ),
226 "lambda.functions" => (
227 factory.from_table(lambda::functions_table(aws)).ok()?,
228 FactoryMode::List,
229 ),
230 "dynamodb.table" => (
231 factory.from_table(dynamodb::tables_table(aws)).ok()?,
232 FactoryMode::Single,
233 ),
234 "dynamodb.tables" => (
235 factory.from_table(dynamodb::tables_table(aws)).ok()?,
236 FactoryMode::List,
237 ),
238 _ => return None,
239 };
240 Some((vista, mode))
241 }
242
243 /// Resolve an ARN to a pre-conditioned single-record [`Vista`].
244 /// Returns `None` if no entity recognises the ARN's resource type.
245 ///
246 /// Dispatch order: each entity's `from_arn` runs in turn — S3 object
247 /// ARNs (`arn:aws:s3:::bucket/key`) are probed before bucket ARNs
248 /// since the object form is a strict superset.
249 pub fn from_arn(&self, arn: &str) -> Option<Vista> {
250 let aws = self.aws.clone();
251 let factory = aws.vista_factory();
252 if let Some(t) = iam::user::User::from_arn(arn, aws.clone()) {
253 return factory.from_table(t).ok();
254 }
255 if let Some(t) = iam::group::Group::from_arn(arn, aws.clone()) {
256 return factory.from_table(t).ok();
257 }
258 if let Some(t) = iam::role::Role::from_arn(arn, aws.clone()) {
259 return factory.from_table(t).ok();
260 }
261 if let Some(t) = iam::policy::Policy::from_arn(arn, aws.clone()) {
262 return factory.from_table(t).ok();
263 }
264 if let Some(t) = iam::instance_profile::InstanceProfile::from_arn(arn, aws.clone()) {
265 return factory.from_table(t).ok();
266 }
267 if let Some(t) = iam::access_key::AccessKey::from_arn(arn, aws.clone()) {
268 return factory.from_table(t).ok();
269 }
270 if let Some(t) = logs::stream::LogStream::from_arn(arn, aws.clone()) {
271 return factory.from_table(t).ok();
272 }
273 if let Some(t) = logs::group::LogGroup::from_arn(arn, aws.clone()) {
274 return factory.from_table(t).ok();
275 }
276 if let Some(t) = ecs::cluster::Cluster::from_arn(arn, aws.clone()) {
277 return factory.from_table(t).ok();
278 }
279 if let Some(t) = s3::object::Object::from_arn(arn, aws.clone()) {
280 return factory.from_table(t).ok();
281 }
282 if let Some(t) = s3::bucket::Bucket::from_arn(arn, aws.clone()) {
283 return factory.from_table(t).ok();
284 }
285 if let Some(t) = lambda::function::Function::from_arn(arn, aws.clone()) {
286 return factory.from_table(t).ok();
287 }
288 if let Some(t) = dynamodb::table::DynamoDbTable::from_arn(arn, aws.clone()) {
289 return factory.from_table(t).ok();
290 }
291 None
292 }
293}