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