Skip to main content

eol_deployer/
deployment_manager.rs

1use crate::{config::Config, docker_client::DockerClient, git_client::GitClient};
2
3pub struct DeploymentManager {
4    docker: DockerClient,
5    git: GitClient,
6}
7
8impl DeploymentManager {
9    pub fn new(socket_path: String) -> Self {
10        Self {
11            docker: DockerClient::new(socket_path),
12            git: GitClient,
13        }
14    }
15
16    pub async fn rolling_deploy(
17        &self,
18        project_name: &str,
19        tag: &str,
20        config: &Config,
21    ) -> Result<(), Box<dyn std::error::Error>> {
22        println!(
23            "Starting rolling deployment for project '{}' with tag '{}'",
24            project_name, tag
25        );
26
27        // 1. Clone the new configuration to a versioned directory
28        let new_config_path = self
29            .git
30            .clone_repository_to_versioned_path(&config.repo_url, tag, &config.mount_path)
31            .await?;
32
33        // 2. Find running Traefik containers for this project
34        let running_containers = self
35            .docker
36            .get_running_traefik_containers(project_name)
37            .await?;
38
39        if running_containers.is_empty() {
40            return Err(format!(
41                "No running Traefik containers found for project '{}'",
42                project_name
43            )
44            .into());
45        }
46
47        println!(
48            "Found {} running Traefik containers",
49            running_containers.len()
50        );
51
52        // 3. For each running container, create a new one with the new config
53        for (index, container) in running_containers.iter().enumerate() {
54            let new_container_name = format!(
55                "{}-{}-{}",
56                container.names[0].trim_start_matches('/'),
57                tag,
58                index
59            );
60
61            println!("Rolling {} -> {}", container.names[0], new_container_name);
62
63            // Create new container with updated volume mount pointing to new config
64            let new_container_id = self
65                .docker
66                .create_container_from_existing(
67                    container,
68                    &new_container_name,
69                    &new_config_path, // Pass the new config path here
70                )
71                .await?;
72
73            // Start the new container
74            self.docker.start_container(&new_container_id).await?;
75
76            // Wait a bit for the new container to be ready
77            tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
78
79            // Health check the new container (simplified)
80            println!("New container {} is starting up...", new_container_name);
81
82            // Stop and remove the old container
83            println!("Stopping old container {}", container.names[0]);
84            self.docker.stop_container(&container.id).await?;
85            self.docker.remove_container(&container.id).await?;
86
87            println!("Successfully rolled {} to new version", container.names[0]);
88        }
89
90        // 4. Clean up old config directories (keep last 3 versions)
91        self.cleanup_old_configs(&config.mount_path, 3).await?;
92
93        println!("Rolling deployment completed successfully!");
94        Ok(())
95    }
96
97    async fn cleanup_old_configs(
98        &self,
99        base_path: &str,
100        keep_versions: usize,
101    ) -> Result<(), Box<dyn std::error::Error>> {
102        let mut config_dirs = Vec::new();
103
104        if let Ok(entries) = std::fs::read_dir(base_path) {
105            for entry in entries {
106                if let Ok(entry) = entry {
107                    let path = entry.path();
108                    if path.is_dir() {
109                        if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
110                            if name.starts_with("traefik-config-") {
111                                config_dirs.push(path);
112                            }
113                        }
114                    }
115                }
116            }
117        }
118
119        // Sort by creation time (newest first)
120        config_dirs.sort_by_key(|path| {
121            std::fs::metadata(path)
122                .and_then(|m| m.created())
123                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
124        });
125        config_dirs.reverse();
126
127        // Remove old versions beyond the keep limit
128        for old_config in config_dirs.iter().skip(keep_versions) {
129            println!("Cleaning up old config: {:?}", old_config);
130            if let Err(e) = std::fs::remove_dir_all(old_config) {
131                eprintln!("Failed to remove old config {:?}: {}", old_config, e);
132            }
133        }
134
135        Ok(())
136    }
137
138    pub async fn rollback(
139        &self,
140        project_name: &str,
141        tag: &str,
142        config: &Config,
143    ) -> Result<(), Box<dyn std::error::Error>> {
144        println!(
145            "Starting rollback of project '{}' to tag '{}'",
146            project_name, tag
147        );
148
149        // Check if the target version already exists
150        let target_config_path = format!("{}/traefik-config-{}", config.mount_path, tag);
151
152        if !std::path::Path::new(&target_config_path).exists() {
153            // If the config doesn't exist locally, clone it
154            println!("Target config not found locally, cloning...");
155            self.git
156                .clone_repository_to_versioned_path(&config.repo_url, tag, &config.mount_path)
157                .await?;
158        } else {
159            println!("Using existing config at {}", target_config_path);
160        }
161
162        // Perform rolling deployment to the target tag
163        self.rolling_deploy(project_name, tag, config).await?;
164
165        println!("Rollback completed successfully!");
166        Ok(())
167    }
168}