Skip to main content

winterbaume_workspaces/
state.rs

1use std::collections::HashMap;
2
3use crate::types::*;
4
5/// In-memory state for the WorkSpaces service.
6#[derive(Debug, Default)]
7pub struct WorkSpacesState {
8    /// Workspaces keyed by workspace ID.
9    pub workspaces: HashMap<String, Workspace>,
10    /// Directories keyed by directory ID.
11    pub directories: HashMap<String, WorkspaceDirectory>,
12    /// Tags keyed by resource ARN.
13    pub tags: HashMap<String, Vec<Tag>>,
14    /// Workspace images keyed by image ID.
15    pub images: HashMap<String, WorkspaceImage>,
16    /// Client properties keyed by resource ID.
17    pub client_properties: HashMap<String, ClientProperties>,
18    /// Self-service permissions keyed by directory ID.
19    pub selfservice_permissions: HashMap<String, SelfservicePermissions>,
20    /// Workspace creation properties keyed by directory ID.
21    pub workspace_creation_properties: HashMap<String, WorkspaceCreationProperties>,
22    /// Image permissions: image_id -> Vec<shared_account_id>
23    pub image_permissions: HashMap<String, Vec<String>>,
24    /// IP groups keyed by group ID.
25    pub ip_groups: HashMap<String, IpGroup>,
26    /// Connection aliases keyed by alias ID.
27    pub connection_aliases: HashMap<String, ConnectionAlias>,
28    /// Connection alias permissions: alias_id -> Vec<ConnectionAliasPermission>
29    pub connection_alias_permissions: HashMap<String, Vec<ConnectionAliasPermission>>,
30    /// Workspace bundles keyed by bundle ID.
31    pub bundles: HashMap<String, WorkspaceBundle>,
32    /// WorkSpaces pools keyed by pool ID.
33    pub pools: HashMap<String, WorkspacesPool>,
34}
35
36/// Error type for WorkSpaces operations.
37#[derive(Debug, thiserror::Error)]
38pub enum WorkSpacesError {
39    #[error("The specified WorkSpace '{0}' was not found.")]
40    WorkspaceNotFound(String),
41
42    #[error("The specified image '{0}' was not found.")]
43    ImageNotFound(String),
44
45    #[error("The specified directory '{0}' was not found.")]
46    DirectoryNotFound(String),
47
48    #[error("The directory '{0}' is already registered.")]
49    DirectoryAlreadyRegistered(String),
50
51    #[error("The specified IP group '{0}' was not found.")]
52    IpGroupNotFound(String),
53
54    #[error("The specified connection alias '{0}' was not found.")]
55    ConnectionAliasNotFound(String),
56
57    #[error("The specified bundle '{0}' was not found.")]
58    BundleNotFound(String),
59
60    #[error("The specified pool '{0}' was not found.")]
61    PoolNotFound(String),
62}
63
64impl WorkSpacesState {
65    /// Create workspaces from a list of requests.
66    /// Returns (successful_workspaces, failed_requests).
67    pub fn create_workspaces(
68        &mut self,
69        requests: &[WorkspaceRequest],
70        account_id: &str,
71        region: &str,
72    ) -> (Vec<Workspace>, Vec<FailedCreateWorkspaceRequest>) {
73        let mut pending = Vec::new();
74        let failed = Vec::new();
75
76        for req in requests {
77            let workspace_id = format!(
78                "ws-{}",
79                &uuid::Uuid::new_v4().to_string().replace('-', "")[..25]
80            );
81            let props = req.workspace_properties.as_ref();
82
83            let workspace = Workspace {
84                workspace_id: workspace_id.clone(),
85                directory_id: req.directory_id.clone(),
86                user_name: req.user_name.clone(),
87                bundle_id: req.bundle_id.clone(),
88                state: "AVAILABLE".to_string(),
89                ip_address: "10.0.0.1".to_string(),
90                computer_name: format!("WSAMZN-{}", &workspace_id[3..10]).to_uppercase(),
91                subnet_id: format!(
92                    "subnet-{}",
93                    &uuid::Uuid::new_v4().to_string().replace('-', "")[..8]
94                ),
95                root_volume_size_gib: props.and_then(|p| p.root_volume_size_gib).unwrap_or(80),
96                user_volume_size_gib: props.and_then(|p| p.user_volume_size_gib).unwrap_or(50),
97                volume_encryption_key: req.volume_encryption_key.clone(),
98                user_volume_encryption_enabled: req.user_volume_encryption_enabled.unwrap_or(false),
99                root_volume_encryption_enabled: req.root_volume_encryption_enabled.unwrap_or(false),
100                running_mode: props
101                    .and_then(|p| p.running_mode.clone())
102                    .unwrap_or_else(|| "AUTO_STOP".to_string()),
103                running_mode_auto_stop_timeout_in_minutes: props
104                    .and_then(|p| p.running_mode_auto_stop_timeout_in_minutes)
105                    .unwrap_or(60),
106            };
107
108            // Ensure the directory exists (auto-create a stub directory if not present)
109            if !self.directories.contains_key(&req.directory_id) {
110                let dir = WorkspaceDirectory {
111                    directory_id: req.directory_id.clone(),
112                    directory_name: format!("corp.{}.com", region),
113                    directory_type: "SIMPLE_AD".to_string(),
114                    alias: req.directory_id.clone(),
115                    state: "REGISTERED".to_string(),
116                    registration_code: format!("SLiad+{}", &uuid::Uuid::new_v4().to_string()[..8]),
117                    workspace_security_group_id: format!(
118                        "sg-{}",
119                        &uuid::Uuid::new_v4().to_string().replace('-', "")[..8]
120                    ),
121                    iam_role_id: format!("arn:aws:iam::{}:role/workspaces_DefaultRole", account_id),
122                };
123                self.directories.insert(req.directory_id.clone(), dir);
124            }
125
126            self.workspaces
127                .insert(workspace_id.clone(), workspace.clone());
128            pending.push(workspace);
129        }
130
131        (pending, failed)
132    }
133
134    /// Describe workspaces, optionally filtered by workspace IDs, directory ID, or user name.
135    pub fn describe_workspaces(
136        &self,
137        workspace_ids: Option<&[String]>,
138        directory_id: Option<&str>,
139        user_name: Option<&str>,
140    ) -> Vec<&Workspace> {
141        self.workspaces
142            .values()
143            .filter(|ws| {
144                if let Some(ids) = workspace_ids {
145                    if !ids.contains(&ws.workspace_id) {
146                        return false;
147                    }
148                }
149                if let Some(dir_id) = directory_id {
150                    if ws.directory_id != dir_id {
151                        return false;
152                    }
153                }
154                if let Some(user) = user_name {
155                    if ws.user_name != user {
156                        return false;
157                    }
158                }
159                true
160            })
161            .collect()
162    }
163
164    /// Terminate workspaces by IDs.
165    /// Returns a list of failed termination requests.
166    pub fn terminate_workspaces(
167        &mut self,
168        requests: &[TerminateRequest],
169    ) -> Vec<FailedTerminateWorkspaceRequest> {
170        let mut failed = Vec::new();
171
172        for req in requests {
173            if self.workspaces.contains_key(&req.workspace_id) {
174                if let Some(ws) = self.workspaces.get_mut(&req.workspace_id) {
175                    ws.state = "TERMINATING".to_string();
176                }
177                // Remove from the map to simulate termination
178                self.workspaces.remove(&req.workspace_id);
179            } else {
180                failed.push(FailedTerminateWorkspaceRequest {
181                    workspace_id: req.workspace_id.clone(),
182                    error_code: "ResourceNotFoundException".to_string(),
183                    error_message: format!(
184                        "The specified WorkSpace '{}' was not found.",
185                        req.workspace_id
186                    ),
187                });
188            }
189        }
190
191        failed
192    }
193
194    /// Describe workspace directories.
195    pub fn describe_workspace_directories(
196        &self,
197        directory_ids: Option<&[String]>,
198    ) -> Vec<&WorkspaceDirectory> {
199        self.directories
200            .values()
201            .filter(|dir| {
202                if let Some(ids) = directory_ids {
203                    if !ids.contains(&dir.directory_id) {
204                        return false;
205                    }
206                }
207                true
208            })
209            .collect()
210    }
211
212    /// Create tags on a resource.
213    pub fn create_tags(&mut self, resource_id: &str, tags: &[Tag]) {
214        let entry = self.tags.entry(resource_id.to_string()).or_default();
215        for tag in tags {
216            // Replace existing tag with same key
217            entry.retain(|t| t.key != tag.key);
218            entry.push(tag.clone());
219        }
220    }
221
222    /// Describe tags for a resource.
223    pub fn describe_tags(&self, resource_id: &str) -> Vec<&Tag> {
224        self.tags
225            .get(resource_id)
226            .map(|tags| tags.iter().collect())
227            .unwrap_or_default()
228    }
229
230    /// Create a workspace image from a workspace.
231    pub fn create_workspace_image(
232        &mut self,
233        name: &str,
234        description: &str,
235        workspace_id: &str,
236        account_id: &str,
237        tags: Option<&[Tag]>,
238    ) -> Result<WorkspaceImage, WorkSpacesError> {
239        if !self.workspaces.contains_key(workspace_id) {
240            return Err(WorkSpacesError::WorkspaceNotFound(workspace_id.to_string()));
241        }
242
243        let image_id = format!(
244            "wsi-{}",
245            &uuid::Uuid::new_v4().to_string().replace('-', "")[..25]
246        );
247
248        let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
249
250        let image = WorkspaceImage {
251            image_id: image_id.clone(),
252            name: name.to_string(),
253            description: description.to_string(),
254            state: "AVAILABLE".to_string(),
255            operating_system_type: Some("WINDOWS".to_string()),
256            owner_account_id: account_id.to_string(),
257            required_tenancy: "DEFAULT".to_string(),
258            created: now,
259        };
260
261        self.images.insert(image_id.clone(), image.clone());
262
263        if let Some(tags) = tags {
264            self.create_tags(&image_id, tags);
265        }
266
267        Ok(image)
268    }
269
270    /// Describe workspace images.
271    pub fn describe_workspace_images(&self, image_ids: Option<&[String]>) -> Vec<&WorkspaceImage> {
272        self.images
273            .values()
274            .filter(|img| {
275                if let Some(ids) = image_ids {
276                    if !ids.contains(&img.image_id) {
277                        return false;
278                    }
279                }
280                true
281            })
282            .collect()
283    }
284
285    /// Describe workspace image permissions.
286    pub fn describe_workspace_image_permissions(
287        &self,
288        image_id: &str,
289    ) -> Result<Vec<String>, WorkSpacesError> {
290        if !self.images.contains_key(image_id) {
291            return Err(WorkSpacesError::ImageNotFound(image_id.to_string()));
292        }
293        Ok(self
294            .image_permissions
295            .get(image_id)
296            .cloned()
297            .unwrap_or_default())
298    }
299
300    /// Update workspace image permission.
301    pub fn update_workspace_image_permission(
302        &mut self,
303        image_id: &str,
304        shared_account_id: &str,
305        allow_copy_image: bool,
306    ) -> Result<(), WorkSpacesError> {
307        if !self.images.contains_key(image_id) {
308            return Err(WorkSpacesError::ImageNotFound(image_id.to_string()));
309        }
310        let perms = self
311            .image_permissions
312            .entry(image_id.to_string())
313            .or_default();
314        if allow_copy_image {
315            if !perms.contains(&shared_account_id.to_string()) {
316                perms.push(shared_account_id.to_string());
317            }
318        } else {
319            perms.retain(|id| id != shared_account_id);
320        }
321        Ok(())
322    }
323
324    /// Describe client properties.
325    pub fn describe_client_properties(
326        &self,
327        resource_ids: &[String],
328    ) -> Vec<(String, ClientProperties)> {
329        resource_ids
330            .iter()
331            .filter_map(|id| {
332                self.client_properties
333                    .get(id)
334                    .map(|cp| (id.clone(), cp.clone()))
335            })
336            .collect()
337    }
338
339    /// Modify client properties for a resource.
340    pub fn modify_client_properties(&mut self, resource_id: &str, props: &ClientProperties) {
341        let entry = self
342            .client_properties
343            .entry(resource_id.to_string())
344            .or_insert_with(|| ClientProperties {
345                reconnect_enabled: None,
346                log_upload_enabled: None,
347            });
348        if let Some(ref v) = props.reconnect_enabled {
349            entry.reconnect_enabled = Some(v.clone());
350        }
351        if let Some(ref v) = props.log_upload_enabled {
352            entry.log_upload_enabled = Some(v.clone());
353        }
354    }
355
356    /// Modify self-service permissions for a directory.
357    pub fn modify_selfservice_permissions(
358        &mut self,
359        directory_id: &str,
360        perms: &SelfservicePermissions,
361    ) -> Result<(), WorkSpacesError> {
362        if !self.directories.contains_key(directory_id) {
363            return Err(WorkSpacesError::DirectoryNotFound(directory_id.to_string()));
364        }
365        self.selfservice_permissions
366            .insert(directory_id.to_string(), perms.clone());
367        Ok(())
368    }
369
370    /// Modify workspace creation properties for a directory.
371    pub fn modify_workspace_creation_properties(
372        &mut self,
373        directory_id: &str,
374        props: &WorkspaceCreationProperties,
375    ) -> Result<(), WorkSpacesError> {
376        if !self.directories.contains_key(directory_id) {
377            return Err(WorkSpacesError::DirectoryNotFound(directory_id.to_string()));
378        }
379        self.workspace_creation_properties
380            .insert(directory_id.to_string(), props.clone());
381        Ok(())
382    }
383
384    /// Register a workspace directory.
385    pub fn register_workspace_directory(
386        &mut self,
387        directory_id: &str,
388        account_id: &str,
389        region: &str,
390    ) -> Result<WorkspaceDirectory, WorkSpacesError> {
391        if self.directories.contains_key(directory_id) {
392            return Err(WorkSpacesError::DirectoryAlreadyRegistered(
393                directory_id.to_string(),
394            ));
395        }
396
397        let dir = WorkspaceDirectory {
398            directory_id: directory_id.to_string(),
399            directory_name: format!("corp.{}.com", region),
400            directory_type: "SIMPLE_AD".to_string(),
401            alias: directory_id.to_string(),
402            state: "REGISTERED".to_string(),
403            registration_code: format!("SLiad+{}", &uuid::Uuid::new_v4().to_string()[..8]),
404            workspace_security_group_id: format!(
405                "sg-{}",
406                &uuid::Uuid::new_v4().to_string().replace('-', "")[..8]
407            ),
408            iam_role_id: format!("arn:aws:iam::{}:role/workspaces_DefaultRole", account_id),
409        };
410
411        self.directories
412            .insert(directory_id.to_string(), dir.clone());
413        Ok(dir)
414    }
415
416    /// Deregister a workspace directory.
417    pub fn deregister_workspace_directory(
418        &mut self,
419        directory_id: &str,
420    ) -> Result<(), WorkSpacesError> {
421        if self.directories.remove(directory_id).is_none() {
422            return Err(WorkSpacesError::DirectoryNotFound(directory_id.to_string()));
423        }
424        Ok(())
425    }
426
427    /// Delete tags from a resource.
428    pub fn delete_tags(&mut self, resource_id: &str, tag_keys: &[String]) {
429        if let Some(tags) = self.tags.get_mut(resource_id) {
430            tags.retain(|t| !tag_keys.contains(&t.key));
431        }
432    }
433
434    /// Start workspaces.
435    pub fn start_workspaces(
436        &mut self,
437        requests: &[StartRequest],
438    ) -> Vec<crate::types::FailedTerminateWorkspaceRequest> {
439        let mut failed = Vec::new();
440        for req in requests {
441            if let Some(ws) = self.workspaces.get_mut(&req.workspace_id) {
442                ws.state = "AVAILABLE".to_string();
443            } else {
444                failed.push(crate::types::FailedTerminateWorkspaceRequest {
445                    workspace_id: req.workspace_id.clone(),
446                    error_code: "ResourceNotFoundException".to_string(),
447                    error_message: format!(
448                        "The specified WorkSpace '{}' was not found.",
449                        req.workspace_id
450                    ),
451                });
452            }
453        }
454        failed
455    }
456
457    /// Stop workspaces.
458    pub fn stop_workspaces(
459        &mut self,
460        requests: &[StopRequest],
461    ) -> Vec<crate::types::FailedTerminateWorkspaceRequest> {
462        let mut failed = Vec::new();
463        for req in requests {
464            if let Some(ws) = self.workspaces.get_mut(&req.workspace_id) {
465                ws.state = "STOPPED".to_string();
466            } else {
467                failed.push(crate::types::FailedTerminateWorkspaceRequest {
468                    workspace_id: req.workspace_id.clone(),
469                    error_code: "ResourceNotFoundException".to_string(),
470                    error_message: format!(
471                        "The specified WorkSpace '{}' was not found.",
472                        req.workspace_id
473                    ),
474                });
475            }
476        }
477        failed
478    }
479
480    /// Reboot workspaces.
481    pub fn reboot_workspaces(
482        &mut self,
483        requests: &[RebootRequest],
484    ) -> Vec<crate::types::FailedTerminateWorkspaceRequest> {
485        let mut failed = Vec::new();
486        for req in requests {
487            if !self.workspaces.contains_key(&req.workspace_id) {
488                failed.push(crate::types::FailedTerminateWorkspaceRequest {
489                    workspace_id: req.workspace_id.clone(),
490                    error_code: "ResourceNotFoundException".to_string(),
491                    error_message: format!(
492                        "The specified WorkSpace '{}' was not found.",
493                        req.workspace_id
494                    ),
495                });
496            }
497        }
498        failed
499    }
500
501    /// Rebuild workspaces.
502    pub fn rebuild_workspaces(
503        &mut self,
504        requests: &[RebuildRequest],
505    ) -> Vec<crate::types::FailedTerminateWorkspaceRequest> {
506        let mut failed = Vec::new();
507        for req in requests {
508            if !self.workspaces.contains_key(&req.workspace_id) {
509                failed.push(crate::types::FailedTerminateWorkspaceRequest {
510                    workspace_id: req.workspace_id.clone(),
511                    error_code: "ResourceNotFoundException".to_string(),
512                    error_message: format!(
513                        "The specified WorkSpace '{}' was not found.",
514                        req.workspace_id
515                    ),
516                });
517            }
518        }
519        failed
520    }
521
522    /// Restore workspace.
523    pub fn restore_workspace(&mut self, workspace_id: &str) -> Result<(), WorkSpacesError> {
524        if !self.workspaces.contains_key(workspace_id) {
525            return Err(WorkSpacesError::WorkspaceNotFound(workspace_id.to_string()));
526        }
527        Ok(())
528    }
529
530    /// Modify workspace properties.
531    pub fn modify_workspace_properties(
532        &mut self,
533        workspace_id: &str,
534        running_mode: Option<&str>,
535        root_volume_size_gib: Option<i32>,
536        user_volume_size_gib: Option<i32>,
537        compute_type_name: Option<&str>,
538        running_mode_auto_stop_timeout_in_minutes: Option<i32>,
539    ) -> Result<(), WorkSpacesError> {
540        let ws = self
541            .workspaces
542            .get_mut(workspace_id)
543            .ok_or_else(|| WorkSpacesError::WorkspaceNotFound(workspace_id.to_string()))?;
544        if let Some(v) = running_mode {
545            ws.running_mode = v.to_string();
546        }
547        if let Some(v) = root_volume_size_gib {
548            ws.root_volume_size_gib = v;
549        }
550        if let Some(v) = user_volume_size_gib {
551            ws.user_volume_size_gib = v;
552        }
553        let _ = compute_type_name; // stored in workspace properties
554        if let Some(v) = running_mode_auto_stop_timeout_in_minutes {
555            ws.running_mode_auto_stop_timeout_in_minutes = v;
556        }
557        Ok(())
558    }
559
560    /// Modify workspace state.
561    pub fn modify_workspace_state(
562        &mut self,
563        workspace_id: &str,
564        workspace_state: &str,
565    ) -> Result<(), WorkSpacesError> {
566        let ws = self
567            .workspaces
568            .get_mut(workspace_id)
569            .ok_or_else(|| WorkSpacesError::WorkspaceNotFound(workspace_id.to_string()))?;
570        ws.state = workspace_state.to_string();
571        Ok(())
572    }
573
574    /// Describe workspaces connection status.
575    pub fn describe_workspaces_connection_status(
576        &self,
577        workspace_ids: Option<&[String]>,
578    ) -> Vec<(&str, &str)> {
579        self.workspaces
580            .values()
581            .filter(|ws| {
582                if let Some(ids) = workspace_ids {
583                    ids.contains(&ws.workspace_id)
584                } else {
585                    true
586                }
587            })
588            .map(|ws| (ws.workspace_id.as_str(), ws.state.as_str()))
589            .collect()
590    }
591
592    // --- IP Groups ---
593
594    /// Create an IP group.
595    pub fn create_ip_group(
596        &mut self,
597        group_name: &str,
598        group_desc: Option<&str>,
599        user_rules: Vec<IpRule>,
600        account_id: &str,
601    ) -> IpGroup {
602        let group_id = format!(
603            "wsipg-{}",
604            &uuid::Uuid::new_v4().to_string().replace('-', "")[..25]
605        );
606        let _ = account_id;
607        let group = IpGroup {
608            group_id: group_id.clone(),
609            group_name: group_name.to_string(),
610            group_desc: group_desc.map(|s| s.to_string()),
611            user_rules,
612        };
613        self.ip_groups.insert(group_id, group.clone());
614        group
615    }
616
617    /// Describe IP groups.
618    pub fn describe_ip_groups(&self, group_ids: Option<&[String]>) -> Vec<&IpGroup> {
619        self.ip_groups
620            .values()
621            .filter(|g| {
622                if let Some(ids) = group_ids {
623                    ids.contains(&g.group_id)
624                } else {
625                    true
626                }
627            })
628            .collect()
629    }
630
631    /// Delete an IP group.
632    pub fn delete_ip_group(&mut self, group_id: &str) -> Result<(), WorkSpacesError> {
633        if self.ip_groups.remove(group_id).is_none() {
634            return Err(WorkSpacesError::IpGroupNotFound(group_id.to_string()));
635        }
636        Ok(())
637    }
638
639    /// Authorize IP rules to an IP group.
640    pub fn authorize_ip_rules(
641        &mut self,
642        group_id: &str,
643        user_rules: Vec<IpRule>,
644    ) -> Result<(), WorkSpacesError> {
645        let group = self
646            .ip_groups
647            .get_mut(group_id)
648            .ok_or_else(|| WorkSpacesError::IpGroupNotFound(group_id.to_string()))?;
649        for rule in user_rules {
650            // Avoid duplicates
651            if !group.user_rules.iter().any(|r| r.ip_rule == rule.ip_rule) {
652                group.user_rules.push(rule);
653            }
654        }
655        Ok(())
656    }
657
658    /// Revoke IP rules from an IP group.
659    pub fn revoke_ip_rules(
660        &mut self,
661        group_id: &str,
662        user_rules: &[String],
663    ) -> Result<(), WorkSpacesError> {
664        let group = self
665            .ip_groups
666            .get_mut(group_id)
667            .ok_or_else(|| WorkSpacesError::IpGroupNotFound(group_id.to_string()))?;
668        group
669            .user_rules
670            .retain(|r| !user_rules.contains(&r.ip_rule));
671        Ok(())
672    }
673
674    /// Update rules of an IP group (replace all rules).
675    pub fn update_rules_of_ip_group(
676        &mut self,
677        group_id: &str,
678        user_rules: Vec<IpRule>,
679    ) -> Result<(), WorkSpacesError> {
680        let group = self
681            .ip_groups
682            .get_mut(group_id)
683            .ok_or_else(|| WorkSpacesError::IpGroupNotFound(group_id.to_string()))?;
684        group.user_rules = user_rules;
685        Ok(())
686    }
687
688    /// Associate IP groups with a directory.
689    pub fn associate_ip_groups(
690        &mut self,
691        directory_id: &str,
692        group_ids: &[String],
693    ) -> Result<(), WorkSpacesError> {
694        if !self.directories.contains_key(directory_id) {
695            return Err(WorkSpacesError::DirectoryNotFound(directory_id.to_string()));
696        }
697        for group_id in group_ids {
698            if !self.ip_groups.contains_key(group_id.as_str()) {
699                return Err(WorkSpacesError::IpGroupNotFound(group_id.to_string()));
700            }
701        }
702        Ok(())
703    }
704
705    /// Disassociate IP groups from a directory.
706    pub fn disassociate_ip_groups(
707        &mut self,
708        directory_id: &str,
709        group_ids: &[String],
710    ) -> Result<(), WorkSpacesError> {
711        if !self.directories.contains_key(directory_id) {
712            return Err(WorkSpacesError::DirectoryNotFound(directory_id.to_string()));
713        }
714        let _ = group_ids;
715        Ok(())
716    }
717
718    // --- Connection Aliases ---
719
720    /// Create a connection alias.
721    pub fn create_connection_alias(
722        &mut self,
723        connection_string: &str,
724        account_id: &str,
725    ) -> ConnectionAlias {
726        let alias_id = format!(
727            "wsca-{}",
728            &uuid::Uuid::new_v4().to_string().replace('-', "")[..25]
729        );
730        let alias = ConnectionAlias {
731            alias_id: alias_id.clone(),
732            connection_string: connection_string.to_string(),
733            owner_account_id: account_id.to_string(),
734            state: "CREATED".to_string(),
735            associations: Vec::new(),
736        };
737        self.connection_aliases.insert(alias_id, alias.clone());
738        alias
739    }
740
741    /// Describe connection aliases.
742    pub fn describe_connection_aliases(
743        &self,
744        alias_ids: Option<&[String]>,
745        resource_id: Option<&str>,
746    ) -> Vec<&ConnectionAlias> {
747        self.connection_aliases
748            .values()
749            .filter(|a| {
750                if let Some(ids) = alias_ids {
751                    if !ids.contains(&a.alias_id) {
752                        return false;
753                    }
754                }
755                if let Some(rid) = resource_id {
756                    if !a
757                        .associations
758                        .iter()
759                        .any(|assoc| assoc.resource_id.as_deref() == Some(rid))
760                    {
761                        return false;
762                    }
763                }
764                true
765            })
766            .collect()
767    }
768
769    /// Delete a connection alias.
770    pub fn delete_connection_alias(&mut self, alias_id: &str) -> Result<(), WorkSpacesError> {
771        if self.connection_aliases.remove(alias_id).is_none() {
772            return Err(WorkSpacesError::ConnectionAliasNotFound(
773                alias_id.to_string(),
774            ));
775        }
776        self.connection_alias_permissions.remove(alias_id);
777        Ok(())
778    }
779
780    /// Associate a connection alias with a resource.
781    pub fn associate_connection_alias(
782        &mut self,
783        alias_id: &str,
784        resource_id: &str,
785    ) -> Result<String, WorkSpacesError> {
786        let alias = self
787            .connection_aliases
788            .get_mut(alias_id)
789            .ok_or_else(|| WorkSpacesError::ConnectionAliasNotFound(alias_id.to_string()))?;
790        let connection_identifier = uuid::Uuid::new_v4().to_string();
791        alias.associations.push(ConnectionAliasAssociation {
792            connection_identifier: connection_identifier.clone(),
793            resource_id: Some(resource_id.to_string()),
794            associated_account_id: None,
795        });
796        alias.state = "ASSOCIATED".to_string();
797        Ok(connection_identifier)
798    }
799
800    /// Disassociate a connection alias.
801    pub fn disassociate_connection_alias(&mut self, alias_id: &str) -> Result<(), WorkSpacesError> {
802        let alias = self
803            .connection_aliases
804            .get_mut(alias_id)
805            .ok_or_else(|| WorkSpacesError::ConnectionAliasNotFound(alias_id.to_string()))?;
806        alias.associations.clear();
807        alias.state = "CREATED".to_string();
808        Ok(())
809    }
810
811    /// Describe connection alias permissions.
812    pub fn describe_connection_alias_permissions(
813        &self,
814        alias_id: &str,
815    ) -> Result<Vec<&ConnectionAliasPermission>, WorkSpacesError> {
816        if !self.connection_aliases.contains_key(alias_id) {
817            return Err(WorkSpacesError::ConnectionAliasNotFound(
818                alias_id.to_string(),
819            ));
820        }
821        Ok(self
822            .connection_alias_permissions
823            .get(alias_id)
824            .map(|v| v.iter().collect())
825            .unwrap_or_default())
826    }
827
828    /// Update connection alias permission.
829    pub fn update_connection_alias_permission(
830        &mut self,
831        alias_id: &str,
832        shared_account_id: &str,
833        allow_association: bool,
834    ) -> Result<(), WorkSpacesError> {
835        if !self.connection_aliases.contains_key(alias_id) {
836            return Err(WorkSpacesError::ConnectionAliasNotFound(
837                alias_id.to_string(),
838            ));
839        }
840        let perms = self
841            .connection_alias_permissions
842            .entry(alias_id.to_string())
843            .or_default();
844        if allow_association {
845            if !perms
846                .iter()
847                .any(|p| p.shared_account_id == shared_account_id)
848            {
849                perms.push(ConnectionAliasPermission {
850                    shared_account_id: shared_account_id.to_string(),
851                    allow_association,
852                });
853            }
854        } else {
855            perms.retain(|p| p.shared_account_id != shared_account_id);
856        }
857        Ok(())
858    }
859
860    // --- Bundles ---
861
862    /// Describe workspace bundles.
863    #[allow(clippy::too_many_arguments)]
864    pub fn describe_workspace_bundles(
865        &self,
866        bundle_ids: Option<&[String]>,
867        owner: Option<&str>,
868    ) -> Vec<&WorkspaceBundle> {
869        self.bundles
870            .values()
871            .filter(|b| {
872                if let Some(ids) = bundle_ids {
873                    if !ids.contains(&b.bundle_id) {
874                        return false;
875                    }
876                }
877                if let Some(o) = owner {
878                    let bundle_owner = b.owner.as_deref().unwrap_or("");
879                    if bundle_owner != o {
880                        return false;
881                    }
882                }
883                true
884            })
885            .collect()
886    }
887
888    /// Create a workspace bundle.
889    pub fn create_workspace_bundle(
890        &mut self,
891        bundle_name: &str,
892        bundle_description: Option<&str>,
893        image_id: &str,
894        compute_type: &str,
895        user_storage: i32,
896        root_storage: Option<i32>,
897        account_id: &str,
898    ) -> Result<WorkspaceBundle, WorkSpacesError> {
899        if !self.images.contains_key(image_id) {
900            return Err(WorkSpacesError::ImageNotFound(image_id.to_string()));
901        }
902        let bundle_id = format!(
903            "wsb-{}",
904            &uuid::Uuid::new_v4().to_string().replace('-', "")[..25]
905        );
906        let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
907        let bundle = WorkspaceBundle {
908            bundle_id: bundle_id.clone(),
909            name: bundle_name.to_string(),
910            owner: Some(account_id.to_string()),
911            description: bundle_description.map(|s| s.to_string()),
912            bundle_type: None,
913            compute_type_name: Some(compute_type.to_string()),
914            root_storage_capacity: root_storage,
915            user_storage_capacity: Some(user_storage),
916            creation_time: now,
917        };
918        self.bundles.insert(bundle_id, bundle.clone());
919        Ok(bundle)
920    }
921
922    /// Delete a workspace bundle.
923    pub fn delete_workspace_bundle(&mut self, bundle_id: &str) -> Result<(), WorkSpacesError> {
924        if self.bundles.remove(bundle_id).is_none() {
925            return Err(WorkSpacesError::BundleNotFound(bundle_id.to_string()));
926        }
927        Ok(())
928    }
929
930    /// Delete a workspace image.
931    pub fn delete_workspace_image(&mut self, image_id: &str) -> Result<(), WorkSpacesError> {
932        if self.images.remove(image_id).is_none() {
933            return Err(WorkSpacesError::ImageNotFound(image_id.to_string()));
934        }
935        self.image_permissions.remove(image_id);
936        Ok(())
937    }
938
939    // --- Pools ---
940
941    /// Create a WorkSpaces pool.
942    pub fn create_workspaces_pool(
943        &mut self,
944        pool_name: &str,
945        description: Option<&str>,
946        bundle_id: &str,
947        directory_id: &str,
948        account_id: &str,
949        region: &str,
950    ) -> WorkspacesPool {
951        let pool_id = format!(
952            "wspool-{}",
953            &uuid::Uuid::new_v4().to_string().replace('-', "")[..25]
954        );
955        let pool_arn = format!(
956            "arn:aws:workspaces:{}:{}:workspacespool/{}",
957            region, account_id, pool_id
958        );
959        let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
960        let pool = WorkspacesPool {
961            pool_id: pool_id.clone(),
962            pool_arn,
963            pool_name: pool_name.to_string(),
964            description: description.map(|s| s.to_string()),
965            state: "RUNNING".to_string(),
966            bundle_id: bundle_id.to_string(),
967            directory_id: directory_id.to_string(),
968            created_at: now,
969        };
970        self.pools.insert(pool_id, pool.clone());
971        pool
972    }
973
974    /// Describe WorkSpaces pools.
975    pub fn describe_workspaces_pools(&self, pool_ids: Option<&[String]>) -> Vec<&WorkspacesPool> {
976        self.pools
977            .values()
978            .filter(|p| {
979                if let Some(ids) = pool_ids {
980                    ids.contains(&p.pool_id)
981                } else {
982                    true
983                }
984            })
985            .collect()
986    }
987
988    /// Terminate a WorkSpaces pool.
989    pub fn terminate_workspaces_pool(
990        &mut self,
991        pool_id: &str,
992    ) -> Result<WorkspacesPool, WorkSpacesError> {
993        let mut pool = self
994            .pools
995            .remove(pool_id)
996            .ok_or_else(|| WorkSpacesError::PoolNotFound(pool_id.to_string()))?;
997        pool.state = "TERMINATING".to_string();
998        Ok(pool)
999    }
1000
1001    /// Update a WorkSpaces pool.
1002    pub fn update_workspaces_pool(
1003        &mut self,
1004        pool_id: &str,
1005        description: Option<&str>,
1006        bundle_id: Option<&str>,
1007        directory_id: Option<&str>,
1008    ) -> Result<WorkspacesPool, WorkSpacesError> {
1009        let pool = self
1010            .pools
1011            .get_mut(pool_id)
1012            .ok_or_else(|| WorkSpacesError::PoolNotFound(pool_id.to_string()))?;
1013        if let Some(v) = description {
1014            pool.description = Some(v.to_string());
1015        }
1016        if let Some(v) = bundle_id {
1017            pool.bundle_id = v.to_string();
1018        }
1019        if let Some(v) = directory_id {
1020            pool.directory_id = v.to_string();
1021        }
1022        Ok(pool.clone())
1023    }
1024
1025    /// Start a WorkSpaces pool.
1026    pub fn start_workspaces_pool(&mut self, pool_id: &str) -> Result<(), WorkSpacesError> {
1027        let pool = self
1028            .pools
1029            .get_mut(pool_id)
1030            .ok_or_else(|| WorkSpacesError::PoolNotFound(pool_id.to_string()))?;
1031        pool.state = "RUNNING".to_string();
1032        Ok(())
1033    }
1034
1035    /// Stop a WorkSpaces pool.
1036    pub fn stop_workspaces_pool(&mut self, pool_id: &str) -> Result<(), WorkSpacesError> {
1037        let pool = self
1038            .pools
1039            .get_mut(pool_id)
1040            .ok_or_else(|| WorkSpacesError::PoolNotFound(pool_id.to_string()))?;
1041        pool.state = "STOPPED".to_string();
1042        Ok(())
1043    }
1044}