1package docker
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/docker/docker/api/types"
12 "github.com/docker/docker/api/types/container"
13 "github.com/docker/docker/api/types/network"
14 "github.com/docker/go-connections/nat"
15)
16
17type ContainerCommandParams struct {
18 ContainerName string
19 ContainerHost string
20 Domain string
21 ServiceName string
22 IsSSL bool
23 EnvVar string
24 ImageName string
25 ImageID string
26 PortMappings []PortMapping
27 Volumes []string
28 Labels []string
29 Network string
30 Restart string
31 Environment []string
32 TraefikEntryPoint string
33}
34
35type PortMapping struct {
36 HostPort string
37 ContainerPort string
38 Protocol string
39}
40
41// ParsePortsSpecs receives a slice of strings in the format "hostPort:containerPort/Protocol" and returns a slice of PortMapping structs
42func ParsePortsSpecs(portsSpecs []string) ([]PortMapping, error) {
43 portMappings := make([]PortMapping, 0, len(portsSpecs))
44
45 for _, portSpec := range portsSpecs {
46 parts := strings.Split(portSpec, ":")
47 if len(parts) != 2 {
48 return nil, fmt.Errorf("invalid port specification: %s. Format should be hostPort:containerPort[/Protocol]", portSpec)
49 }
50
51 // Validate hostPort and containerPort
52 hostPort := parts[0]
53 if _, err := strconv.Atoi(hostPort); err != nil {
54 return nil, fmt.Errorf("invalid host port: %s. Must be a number", hostPort)
55 }
56
57 containerParts := strings.Split(parts[1], "/")
58 containerPort := containerParts[0]
59 if _, err := strconv.Atoi(containerPort); err != nil {
60 return nil, fmt.Errorf("invalid exposed port: %s. Must be a number", containerPort)
61 }
62
63 // Default protocol is tcp
64 protocol := "tcp"
65 if len(containerParts) > 1 {
66 protocol = containerParts[1]
67 }
68
69 portMappings = append(portMappings, PortMapping{
70 HostPort: hostPort,
71 ContainerPort: containerPort,
72 Protocol: protocol,
73 })
74 }
75
76 return portMappings, nil
77}
78
79func ContainerCommandParamsToConfig(cmdParams ContainerCommandParams) (*container.Config, error) {
80 return &container.Config{
81 Image: cmdParams.ImageName,
82 Hostname: cmdParams.ContainerHost,
83 Env: cmdParams.Environment,
84 Labels: map[string]string{},
85 Volumes: map[string]struct{}{},
86 }, nil
87}
88
89// ListRunningContainers lists all running containers
90func ListRunningContainers() ([]types.Container, error) {
91 // Check if the Docker client has been initialized
92 err := CheckIfInitialized()
93 if err != nil {
94 return nil, err
95 }
96 // List containers using the Docker client:
97 containers, err := dockerCli.ContainerList(context.Background(), container.ListOptions{All: true})
98 if err != nil {
99 return nil, err
100 }
101 // Check if the list is empty
102 if len(containers) == 0 {
103 return nil, fmt.Errorf("no containers found")
104 }
105 return containers, nil
106}
107
108// StopContainer try to stop a container gracefully, if it fails, it will stop it forcefully
109func StopContainer(containerID string) error {
110 StopContainerGracefully(containerID, 3*time.Second)
111 StopContainerRagefully(containerID)
112 return nil
113}
114
115// StopContainerGracefully stops a container by sending a SIGTERM and waiting for it to stop
116func StopContainerGracefully(containerID string, timeoutDuration time.Duration) (bool, error) {
117 // Start by sending a SIGTERM
118 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGTERM")
119 if err != nil {
120 return false, err
121 }
122
123 // Initialize a ticker for timeout
124 ticker := time.NewTicker(1 * time.Second)
125 defer ticker.Stop()
126
127 for elapsed := 0; elapsed < int(timeoutDuration.Seconds()); elapsed++ {
128 select {
129 case <-ticker.C:
130 // Check if the container is still running
131 container, err := dockerCli.ContainerInspect(context.Background(), containerID)
132 if err != nil {
133 return false, err
134 }
135
136 // If the container is not running, return true indicating it was stopped
137 if !container.State.Running {
138 return true, nil
139 }
140 }
141 }
142
143 // Return false, signaling that the container needs to be force-stopped
144 return false, nil
145}
146
147// StopContainerRagefully stops a container by sending a SIGKILL
148func StopContainerRagefully(containerID string) error {
149 // Start by sending a SIGKILL
150 err := dockerCli.ContainerKill(context.Background(), containerID, "SIGKILL")
151 if err != nil {
152 return err
153 }
154
155 return nil
156}
157
158// RenameContainer renames a container with the given name
159func RenameContainer(containerID string, newName string) error {
160 // Rename container using the Docker client
161 err := dockerCli.ContainerRename(context.Background(), containerID, newName)
162 if err != nil {
163 return err
164 }
165
166 return nil
167}
168
169// RemoveContainer
170func RemoveContainer(containerID string) error {
171 err := dockerCli.ContainerRemove(context.Background(), containerID, container.RemoveOptions{Force: true})
172 if err != nil {
173 return err
174 }
175
176 return nil
177}
178
179// StartContainer starts a container
180func StartContainer(containerID string) error {
181 fmt.Println("Starting container", containerID)
182 // Check if the container is not already in a running state
183 containerInfo, err := GetContainerInfo(containerID)
184 if err != nil {
185 return fmt.Errorf("could not get container info: %v", err)
186 }
187
188 if containerInfo.State.Running {
189 return fmt.Errorf("container is already running")
190 }
191
192 // Start container using the Docker client
193 err = dockerCli.ContainerStart(context.Background(), containerID, container.StartOptions{})
194 if err != nil {
195 return fmt.Errorf("could not start container: %v", err)
196 }
197
198 return nil
199}
200
201// CreateContainer creates a container with the given parameters
202func CreateContainer(cmdParams ContainerCommandParams) (string, error) {
203 // Check if the Docker client has been initialized
204 CheckIfInitialized()
205
206 isNetworkCreated, err := CheckIfNetworkExists(cmdParams.Network)
207 if err != nil {
208 return "", err
209 }
210
211 if !isNetworkCreated {
212 _, err := dockerCli.NetworkCreate(context.Background(), cmdParams.Network, types.NetworkCreate{})
213 if err != nil {
214 return "", err
215 }
216 }
217
218 // if the network exist return its id
219 // networkInfo, err := GetNetworkInfo(cmdParams.Network)
220 // if err != nil {
221 // return "", err
222
223 // }
224
225 // fmt.Print(networkInfo.ID)
226
227 // Prepare port bindings
228 portBindings := nat.PortMap{}
229 for _, portMapping := range cmdParams.PortMappings {
230 // Prepare exposed port
231 exposedPort := nat.Port(portMapping.ContainerPort + "/" + portMapping.Protocol)
232 portBindings[exposedPort] = []nat.PortBinding{
233 {
234 HostIP: "0.0.0.0",
235 HostPort: portMapping.HostPort,
236 },
237 }
238 }
239
240 // Prepare labels for Traefik
241 labels := map[string]string{}
242 for _, label := range cmdParams.Labels {
243 keyValue := strings.Split(label, "=")
244 if len(keyValue) == 2 {
245 labels[keyValue[0]] = keyValue[1]
246 }
247 }
248
249 // Prepare environment variables
250 envVars := append([]string{}, cmdParams.Environment...)
251
252 // Create container
253 resp, err := dockerCli.ContainerCreate(
254 context.Background(),
255 &container.Config{
256 Image: cmdParams.ImageName,
257 Labels: labels,
258 Env: envVars,
259 },
260 &container.HostConfig{
261 PortBindings: portBindings,
262 Binds: cmdParams.Volumes,
263 RestartPolicy: container.RestartPolicy{
264 Name: "always",
265 },
266 },
267 &network.NetworkingConfig{
268 EndpointsConfig: map[string]*network.EndpointSettings{
269 cmdParams.Network: {},
270 },
271 },
272 nil,
273 cmdParams.ContainerName,
274 )
275 if err != nil {
276 return "", err
277 }
278
279 return resp.ID, nil
280}
281
282func CheckIfNetworkExists(networkName string) (bool, error) {
283 networks, err := dockerCli.NetworkList(context.Background(), types.NetworkListOptions{})
284 if err != nil {
285 return false, err
286 }
287
288 for _, network := range networks {
289 if network.Name == networkName {
290 return true, nil
291 }
292 }
293
294 return false, nil
295}
296
297// GetNetworkInfo returns information about a network
298func GetNetworkInfo(networkName string) (types.NetworkResource, error) {
299 // Get network info using the Docker client
300 networkInfo, err := dockerCli.NetworkInspect(context.Background(), networkName, types.NetworkInspectOptions{})
301 if err != nil {
302 return types.NetworkResource{}, err
303 }
304
305 return networkInfo, nil
306}
307
308// GetContainerInfo returns information about a container
309func GetContainerInfo(containerID string) (types.ContainerJSON, error) {
310 // Get container info using the Docker client
311 containerInfo, err := dockerCli.ContainerInspect(context.Background(), containerID)
312 if err != nil {
313 return types.ContainerJSON{}, err
314 }
315
316 return containerInfo, nil
317}
318
319// UpdateContainerConfig updates the configuration of an existing container.
320func UpdateContainerConfig(containerID string, newConfig *container.Config, newHostConfig *container.HostConfig, newNetworkingConfig *network.NetworkingConfig) error {
321 ctx := context.Background()
322 // 1. Gracefully stop the existing container
323 _, err := StopContainerGracefully(containerID, 3*time.Second)
324 if err != nil {
325 return err
326 }
327
328 // 2. Remove the old container
329 if err := dockerCli.ContainerRemove(ctx, containerID, container.RemoveOptions{}); err != nil {
330 return err
331 }
332
333 // 3. Create a new container with the new configuration
334 resp, err := dockerCli.ContainerCreate(
335 ctx,
336 newConfig,
337 &container.HostConfig{},
338 &network.NetworkingConfig{},
339 nil,
340 "",
341 )
342 if err != nil {
343 return err
344 }
345
346 // 4. Start the new container
347 if err := dockerCli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
348 return err
349 }
350
351 return nil
352}
353
354// GetContainerLogs returns the logs of a container
355func GetContainerLogs(containerID string) (string, error) {
356 // Get container logs using the Docker client
357 containerLogs, err := dockerCli.ContainerLogs(context.Background(), containerID, container.LogsOptions{ShowStdout: true, ShowStderr: true})
358 if err != nil {
359 return "", err
360 }
361
362 // Read the logs
363 buf := new(bytes.Buffer)
364 buf.ReadFrom(containerLogs)
365 containerLogsString := buf.String()
366
367 return containerLogsString, nil
368}
369
370// CheckContainerStatus checks if a container with the given name exists and is running
371func CheckContainerStatus(containerName string) (bool, bool, error) {
372 // List all containers
373 containers, err := ListRunningContainers()
374 if err != nil {
375 return false, false, err
376 }
377
378 // Check if a container with the given name exists and is running
379 for _, container := range containers {
380 for _, name := range container.Names {
381 if strings.TrimLeft(name, "/") == containerName {
382 return true, container.State == "running", nil
383 }
384 }
385 }
386
387 return false, false, nil
388}