Skip to main content

eol_deployer/
docker_client.rs

1use std::io::{Read, Write};
2use std::os::unix::net::UnixStream;
3
4use crate::types::Container;
5
6pub struct DockerClient {
7    socket_path: String,
8}
9
10impl DockerClient {
11    pub fn new(socket_path: String) -> Self {
12        Self { socket_path }
13    }
14
15    async fn api_call(&self, endpoint: &str) -> Result<String, Box<dyn std::error::Error>> {
16        let stream = UnixStream::connect(&self.socket_path)?;
17        self.send_request(stream, endpoint).await
18    }
19
20    async fn send_request(
21        &self,
22        mut stream: UnixStream,
23        endpoint: &str,
24    ) -> Result<String, Box<dyn std::error::Error>> {
25        let request = format!(
26            "GET {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
27            endpoint
28        );
29
30        stream.write_all(request.as_bytes())?;
31        self.read_response(stream)
32    }
33
34    fn read_response(&self, mut stream: UnixStream) -> Result<String, Box<dyn std::error::Error>> {
35        let mut response = String::new();
36        stream.read_to_string(&mut response)?;
37
38        // Clean up HTTP chunked encoding and extract JSON body
39        if let Some(json_start) = response.find("\r\n\r\n") {
40            let body = &response[json_start + 4..];
41            // Handle chunked encoding - remove chunk size markers
42            Ok(self.clean_chunked_response(body))
43        } else {
44            Ok(response)
45        }
46    }
47
48    fn clean_chunked_response(&self, body: &str) -> String {
49        // Remove HTTP chunked encoding artifacts
50        let mut cleaned = body.to_string();
51
52        // Remove chunk size at the beginning (like "f053\r\n")
53        if let Some(first_newline) = cleaned.find("\r\n") {
54            if cleaned[..first_newline]
55                .chars()
56                .all(|c| c.is_ascii_hexdigit())
57            {
58                cleaned = cleaned[first_newline + 2..].to_string();
59            }
60        }
61
62        // Remove trailing chunk markers (like "\r\n0\r\n\r\n")
63        if cleaned.ends_with("\r\n0\r\n\r\n") {
64            cleaned.truncate(cleaned.len() - 7);
65        } else if cleaned.ends_with("\n\r\n0\r\n\r\n") {
66            cleaned.truncate(cleaned.len() - 8);
67        }
68
69        cleaned
70    }
71
72    pub async fn list_containers(
73        &self,
74        all: bool,
75    ) -> Result<Vec<Container>, Box<dyn std::error::Error>> {
76        let endpoint = if all {
77            "/containers/json?all=true"
78        } else {
79            "/containers/json"
80        };
81        let json_response = self.api_call(endpoint).await?;
82        let containers: Vec<Container> = serde_json::from_str(&json_response)?;
83        Ok(containers)
84    }
85
86    pub async fn get_containers_by_project(
87        &self,
88        project_name: &str,
89    ) -> Result<Vec<Container>, Box<dyn std::error::Error>> {
90        let containers = self.list_containers(true).await?;
91        Ok(containers
92            .into_iter()
93            .filter(|container| {
94                if let Some(ref labels) = container.labels {
95                    labels.get("com.docker.compose.project") == Some(&project_name.to_string())
96                } else {
97                    false
98                }
99            })
100            .collect())
101    }
102
103    pub async fn get_running_traefik_containers(
104        &self,
105        project_name: &str,
106    ) -> Result<Vec<Container>, Box<dyn std::error::Error>> {
107        let containers = self.get_containers_by_project(project_name).await?;
108        Ok(containers
109            .into_iter()
110            .filter(|container| container.state == "running" && container.image.contains("traefik"))
111            .collect())
112    }
113
114    pub async fn create_container_from_existing(
115        &self,
116        template_container: &Container,
117        new_name: &str,
118        new_config_path: &str,
119    ) -> Result<String, Box<dyn std::error::Error>> {
120        // This is a simplified version - in reality you'd need to recreate the full container config
121        // For now, we'll assume you're using docker-compose and can scale the service
122        println!(
123            "Creating new container {} based on {} with config path {}",
124            new_name, template_container.names[0], new_config_path
125        );
126
127        // In a real implementation, you would:
128        // 1. Get the full container configuration
129        // 2. Create a new container with updated volume mounts pointing to new_config_path
130        // 3. Update the volume mount to point to the new versioned config directory
131        // 4. Return the new container ID
132
133        // Example of what the volume mount update would look like:
134        // OLD: /opt/traefik-configs/current:/etc/traefik
135        // NEW: /opt/traefik-configs/traefik-config-v1.2.3:/etc/traefik
136
137        // For now, returning a placeholder - you'll need to implement the full container creation logic
138        Ok("new_container_id".to_string())
139    }
140
141    pub async fn remove_container(
142        &self,
143        container_id: &str,
144    ) -> Result<(), Box<dyn std::error::Error>> {
145        let endpoint = &format!("/containers/{}?force=true", container_id);
146        let stream = UnixStream::connect(&self.socket_path)?;
147        self.send_delete_request(stream, endpoint).await?;
148        Ok(())
149    }
150
151    async fn send_delete_request(
152        &self,
153        mut stream: UnixStream,
154        endpoint: &str,
155    ) -> Result<String, Box<dyn std::error::Error>> {
156        let request = format!(
157            "DELETE {} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n",
158            endpoint
159        );
160
161        stream.write_all(request.as_bytes())?;
162        self.read_response(stream)
163    }
164
165    pub async fn stop_container(
166        &self,
167        container_id: &str,
168    ) -> Result<(), Box<dyn std::error::Error>> {
169        let endpoint = &format!("/containers/{}/stop", container_id);
170        let stream = UnixStream::connect(&self.socket_path)?;
171        self.send_post_request(stream, endpoint, "").await?;
172        Ok(())
173    }
174
175    pub async fn start_container(
176        &self,
177        container_id: &str,
178    ) -> Result<(), Box<dyn std::error::Error>> {
179        let endpoint = &format!("/containers/{}/start", container_id);
180        let stream = UnixStream::connect(&self.socket_path)?;
181        self.send_post_request(stream, endpoint, "").await?;
182        Ok(())
183    }
184
185    async fn send_post_request(
186        &self,
187        mut stream: UnixStream,
188        endpoint: &str,
189        body: &str,
190    ) -> Result<String, Box<dyn std::error::Error>> {
191        let request = format!(
192            "POST {} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
193            endpoint,
194            body.len(),
195            body
196        );
197
198        stream.write_all(request.as_bytes())?;
199        self.read_response(stream)
200    }
201}