rusticity_term/aws/
profile.rs

1use crate::common::SortDirection;
2use crate::ui::active_border;
3use crate::ui::table::{render_table, Column as TableColumn, TableConfig};
4use ratatui::{prelude::*, widgets::*};
5
6pub struct Profile {
7    pub name: String,
8    pub region: Option<String>,
9    pub account: Option<String>,
10    pub role_arn: Option<String>,
11    pub source_profile: Option<String>,
12}
13
14impl Profile {
15    pub fn load_all() -> Vec<Self> {
16        let mut profiles = Vec::new();
17        let home = std::env::var("HOME").unwrap_or_default();
18        let config_path = format!("{}/.aws/config", home);
19        let credentials_path = format!("{}/.aws/credentials", home);
20
21        // Parse config file
22        if let Ok(content) = std::fs::read_to_string(&config_path) {
23            let mut current_profile: Option<String> = None;
24            let mut current_region: Option<String> = None;
25            let mut current_role: Option<String> = None;
26            let mut current_source: Option<String> = None;
27
28            for line in content.lines() {
29                let line = line.trim();
30                if line.starts_with('[') && line.ends_with(']') {
31                    if let Some(name) = current_profile.take() {
32                        profiles.push(Profile {
33                            name,
34                            region: current_region.take(),
35                            account: None,
36                            role_arn: current_role.take(),
37                            source_profile: current_source.take(),
38                        });
39                    }
40                    let profile_name = line
41                        .trim_start_matches('[')
42                        .trim_end_matches(']')
43                        .trim_start_matches("profile ")
44                        .to_string();
45                    current_profile = Some(profile_name);
46                } else if let Some(key_value) = line.split_once('=') {
47                    let key = key_value.0.trim();
48                    let value = key_value.1.trim().to_string();
49                    match key {
50                        "region" => current_region = Some(value),
51                        "role_arn" => current_role = Some(value),
52                        "source_profile" => current_source = Some(value),
53                        _ => {}
54                    }
55                }
56            }
57            if let Some(name) = current_profile {
58                profiles.push(Profile {
59                    name,
60                    region: current_region,
61                    account: None,
62                    role_arn: current_role,
63                    source_profile: current_source,
64                });
65            }
66        }
67
68        // Parse credentials file for additional profiles
69        if let Ok(content) = std::fs::read_to_string(&credentials_path) {
70            for line in content.lines() {
71                let line = line.trim();
72                if line.starts_with('[') && line.ends_with(']') {
73                    let profile_name = line
74                        .trim_start_matches('[')
75                        .trim_end_matches(']')
76                        .to_string();
77                    if !profiles.iter().any(|p| p.name == profile_name) {
78                        profiles.push(Profile {
79                            name: profile_name,
80                            region: None,
81                            account: None,
82                            role_arn: None,
83                            source_profile: None,
84                        });
85                    }
86                }
87            }
88        }
89
90        profiles
91    }
92}
93
94pub fn render_profile_picker(
95    frame: &mut ratatui::Frame,
96    app: &crate::app::App,
97    area: ratatui::prelude::Rect,
98    centered_rect: fn(u16, u16, ratatui::prelude::Rect) -> ratatui::prelude::Rect,
99) {
100    let popup_area = centered_rect(80, 70, area);
101
102    let chunks = Layout::default()
103        .direction(Direction::Vertical)
104        .constraints([Constraint::Length(3), Constraint::Min(0)])
105        .split(popup_area);
106
107    let cursor = "█";
108    let filter = Paragraph::new(Line::from(vec![
109        Span::raw(&app.profile_filter),
110        Span::styled(cursor, Style::default().fg(Color::Green)),
111    ]))
112    .block(
113        Block::default()
114            .title(" 🔍 ")
115            .borders(Borders::ALL)
116            .border_style(active_border()),
117    )
118    .style(Style::default());
119
120    frame.render_widget(Clear, popup_area);
121    frame.render_widget(filter, chunks[0]);
122
123    struct ProfileNameColumn;
124    impl TableColumn<Profile> for ProfileNameColumn {
125        fn name(&self) -> &str {
126            "Profile"
127        }
128        fn width(&self) -> u16 {
129            25
130        }
131        fn render(&self, item: &Profile) -> (String, Style) {
132            (item.name.clone(), Style::default())
133        }
134    }
135
136    struct ProfileAccountColumn;
137    impl TableColumn<Profile> for ProfileAccountColumn {
138        fn name(&self) -> &str {
139            "Account"
140        }
141        fn width(&self) -> u16 {
142            15
143        }
144        fn render(&self, item: &Profile) -> (String, Style) {
145            (item.account.clone().unwrap_or_default(), Style::default())
146        }
147    }
148
149    struct ProfileRegionColumn;
150    impl TableColumn<Profile> for ProfileRegionColumn {
151        fn name(&self) -> &str {
152            "Region"
153        }
154        fn width(&self) -> u16 {
155            15
156        }
157        fn render(&self, item: &Profile) -> (String, Style) {
158            (item.region.clone().unwrap_or_default(), Style::default())
159        }
160    }
161
162    struct ProfileRoleColumn;
163    impl TableColumn<Profile> for ProfileRoleColumn {
164        fn name(&self) -> &str {
165            "Role/User"
166        }
167        fn width(&self) -> u16 {
168            30
169        }
170        fn render(&self, item: &Profile) -> (String, Style) {
171            if let Some(ref role) = item.role_arn {
172                if role.contains(":role/") {
173                    let role_name = role.split('/').next_back().unwrap_or(role);
174                    (format!("role/{}", role_name), Style::default())
175                } else if role.contains(":user/") {
176                    let user_name = role.split('/').next_back().unwrap_or(role);
177                    (format!("user/{}", user_name), Style::default())
178                } else {
179                    (role.clone(), Style::default())
180                }
181            } else {
182                (String::new(), Style::default())
183            }
184        }
185    }
186
187    struct ProfileSourceColumn;
188    impl TableColumn<Profile> for ProfileSourceColumn {
189        fn name(&self) -> &str {
190            "Source"
191        }
192        fn width(&self) -> u16 {
193            20
194        }
195        fn render(&self, item: &Profile) -> (String, Style) {
196            (
197                item.source_profile.clone().unwrap_or_default(),
198                Style::default(),
199            )
200        }
201    }
202
203    let columns: Vec<Box<dyn TableColumn<Profile>>> = vec![
204        Box::new(ProfileNameColumn),
205        Box::new(ProfileAccountColumn),
206        Box::new(ProfileRegionColumn),
207        Box::new(ProfileRoleColumn),
208        Box::new(ProfileSourceColumn),
209    ];
210
211    let filtered = app.get_filtered_profiles();
212    let config = TableConfig {
213        items: filtered,
214        selected_index: app.profile_picker_selected,
215        expanded_index: None,
216        columns: &columns,
217        sort_column: "Profile",
218        sort_direction: SortDirection::Asc,
219        title: " Profiles (^R to fetch accounts) ".to_string(),
220        area: chunks[1],
221        get_expanded_content: None,
222        is_active: true,
223    };
224
225    render_table(frame, config);
226}
227
228pub fn filter_profiles<'a>(profiles: &'a [Profile], filter: &str) -> Vec<&'a Profile> {
229    let mut filtered: Vec<&Profile> = if filter.is_empty() {
230        profiles.iter().collect()
231    } else {
232        let filter_lower = filter.to_lowercase();
233        profiles
234            .iter()
235            .filter(|p| {
236                p.name.to_lowercase().contains(&filter_lower)
237                    || p.region
238                        .as_ref()
239                        .is_some_and(|r| r.to_lowercase().contains(&filter_lower))
240                    || p.account
241                        .as_ref()
242                        .is_some_and(|a| a.to_lowercase().contains(&filter_lower))
243                    || p.role_arn
244                        .as_ref()
245                        .is_some_and(|r| r.to_lowercase().contains(&filter_lower))
246                    || p.source_profile
247                        .as_ref()
248                        .is_some_and(|s| s.to_lowercase().contains(&filter_lower))
249            })
250            .collect()
251    };
252    filtered.sort_by(|a, b| a.name.cmp(&b.name));
253    filtered
254}