1package handler
2
3import (
4 "fmt"
5 "log"
6 "strings"
7
8 "github.com/bnema/gordon/internal/server"
9 "github.com/bnema/gordon/internal/templating/cmdparams"
10 "github.com/bnema/gordon/internal/templating/render"
11 "github.com/bnema/gordon/pkg/docker"
12 "github.com/labstack/echo/v4"
13 "gopkg.in/yaml.v3"
14)
15
16func generateOrderedYAML(infoMap map[string]interface{}) (string, error) {
17 orderedKeys := []string{"Name", "Image", "Hostname", "Ports", "Environment", "Labels", "Network", "Volumes", "Restart"}
18
19 var yamlString string
20 for _, key := range orderedKeys {
21 value, exists := infoMap[key]
22 if !exists {
23 continue
24 }
25
26 yamlBytes, err := yaml.Marshal(map[string]interface{}{key: value})
27 if err != nil {
28 return "", err
29 }
30
31 yamlString += string(yamlBytes)
32 }
33
34 return yamlString, nil
35}
36
37// ContainerManagerEditGET displays the edit container view
38func ContainerManagerEditGET(c echo.Context, a *server.App) error {
39 containerID := c.Param("ID")
40
41 // Get the container info
42 containerInfo, err := docker.GetContainerInfo(containerID)
43 if err != nil {
44 return sendError(c, err)
45 }
46
47 // Trim the slash from the container name
48 containerInfo.Name = strings.TrimPrefix(containerInfo.Name, "/")
49 // Get the network names
50 networkNames := make([]string, 0, len(containerInfo.NetworkSettings.Networks))
51 for networkName := range containerInfo.NetworkSettings.Networks {
52 networkNames = append(networkNames, networkName)
53 }
54
55 // Prepare Ports
56 portMappings := make([]string, 0)
57 for port, bindings := range containerInfo.HostConfig.PortBindings {
58 for _, binding := range bindings {
59 portMappings = append(portMappings, fmt.Sprintf("%s:%s/%s", binding.HostPort, port.Port(), port.Proto()))
60 }
61 }
62
63 // Prepare Volumes (Mounts)
64 volumeMappings := make([]string, 0)
65 for _, mount := range containerInfo.Mounts {
66 volumeMappings = append(volumeMappings, fmt.Sprintf("%s:%s", mount.Source, mount.Destination))
67 }
68
69 // Initialize a map to store the information from the container
70 infoMap := make(map[string]interface{})
71
72 // Populate the map with container details
73 infoMap["Name"] = containerInfo.Name
74 infoMap["Image"] = containerInfo.Config.Image
75 infoMap["Hostname"] = containerInfo.Config.Hostname
76 infoMap["Ports"] = portMappings
77 infoMap["Volumes"] = volumeMappings
78 infoMap["Environment"] = containerInfo.Config.Env
79 infoMap["Labels"] = containerInfo.Config.Labels
80 infoMap["Network"] = networkNames
81 infoMap["Restart"] = containerInfo.HostConfig.RestartPolicy.Name
82
83 // Convert the infoMap to YAML format
84 yamlString, err := generateOrderedYAML(infoMap)
85 if err != nil {
86 return sendError(c, err)
87 }
88
89 // Pass the containerInfo to the data map
90 data := map[string]interface{}{
91 "Title": "Edit container",
92 "ID": containerID,
93 "ContainerInfo": yamlString,
94 }
95
96 rendererData, err := render.GetHTMLRenderer("html/fragments", "editcontainer.gohtml", a.TemplateFS, a)
97
98 if err != nil {
99 return sendError(c, err)
100 }
101
102 renderedHTML, err := rendererData.Render(data, a)
103
104 if err != nil {
105 return sendError(c, err)
106 }
107
108 return c.HTML(200, renderedHTML)
109}
110
111type TransactionStep func() error
112type RollbackStep func() error
113
114type TransactionQueue struct {
115 NewContainerID string
116 NewContainerName string
117 TempContainerName string
118 OldContainerID string
119 OldContainerName string
120 steps []TransactionStep
121 rollbacks []RollbackStep
122}
123
124func (tq *TransactionQueue) Add(step TransactionStep, rollback RollbackStep) {
125 tq.steps = append(tq.steps, step)
126 tq.rollbacks = append(tq.rollbacks, rollback)
127}
128
129func (tq *TransactionQueue) Execute() error {
130 var transactionErrors []error
131 var rollbackErrors []error
132 for i, step := range tq.steps {
133 err := step()
134 if err != nil {
135 log.Printf("Error occurred at step index %d: %v", i, err)
136 transactionErrors = append(transactionErrors, err)
137 for j := i - 1; j >= 0; j-- {
138 rollback := tq.rollbacks[j]
139 if rollback != nil {
140 if rollbackErr := rollback(); rollbackErr != nil {
141 log.Printf("Failed to execute rollback at index %d: %v", j, rollbackErr)
142 rollbackErrors = append(rollbackErrors, rollbackErr)
143 }
144 }
145 }
146 return fmt.Errorf("transaction failed: %v; rollback errors: %v", transactionErrors, rollbackErrors)
147 }
148 }
149 return nil
150}
151
152// ContainerManagerEditPOST handles the edit container form submission
153func ContainerManagerEditPOST(c echo.Context, a *server.App) error {
154 tq := &TransactionQueue{}
155 oldContainerID := c.Param("ID")
156
157 oldContainerInfo, err := docker.GetContainerInfo(oldContainerID)
158 if err != nil {
159 return sendError(c, fmt.Errorf("getting old container info failed: %w", err))
160 }
161
162 containerConfig := c.FormValue("container_config")
163 var containerParams cmdparams.YAMLContainerParams
164 err = yaml.Unmarshal([]byte(containerConfig), &containerParams)
165 if err != nil {
166 return sendError(c, fmt.Errorf("YAML unmarshal failed: %w", err))
167 }
168
169 // Use render.FromYAMLStructToCmdParams to convert YAMLContainerParams to ContainerCommandParams
170 cmdParams, err := cmdparams.FromYAMLStructToCmdParams(containerParams)
171 if err != nil {
172 return fmt.Errorf("failed to convert YAML: %w", err)
173 }
174
175 tq.OldContainerID = oldContainerID
176 tq.OldContainerName = strings.TrimPrefix(oldContainerInfo.Name, "/")
177 tq.NewContainerName = cmdParams.ContainerName
178 tq.TempContainerName = fmt.Sprintf("%s-temp", tq.NewContainerName)
179
180 cmdParams.ContainerName = tq.TempContainerName
181
182 // 1. Stop the old container
183 tq.Add(StopOldContainerStep(tq), StartOldContainerRollback(tq))
184
185 // 2. Create the new container
186 tq.Add(CreateNewContainerStep(tq, cmdParams), RemoveNewContainerRollback(tq))
187
188 // 3. Start the new container
189 tq.Add(StartNewContainerStep(tq), StopNewContainerRollback(tq))
190
191 // 4. Rename the new container to the original name
192 tq.Add(RemoveOldContainerStep(tq), nil)
193
194 // 5. Remove the old container
195 tq.Add(RenameNewContainerStep(tq), RenameNewContainerRollback(tq))
196
197 err = tq.Execute()
198 if err != nil {
199 return sendError(c, fmt.Errorf("transaction failed: %w", err))
200 }
201
202 return c.HTML(200, ActionSuccess(a))
203}
204
205// CreateNewContainerStep creates a new container with the given parameters
206func CreateNewContainerStep(tq *TransactionQueue, cmdParams docker.ContainerCommandParams) TransactionStep {
207 return func() error {
208 var err error
209 tq.NewContainerID, err = docker.CreateContainer(cmdParams)
210 if err != nil {
211 return fmt.Errorf("creating new container failed: %w", err)
212 }
213 return nil
214 }
215}
216
217// StartNewContainerStep starts the new container
218func StartNewContainerStep(tq *TransactionQueue) TransactionStep {
219 return func() error {
220 err := docker.StartContainer(tq.NewContainerID)
221 if err != nil {
222 return fmt.Errorf("starting new container failed: %w", err)
223 }
224 return nil
225 }
226}
227
228// StopOldContainerStep stops the old container
229func StopOldContainerStep(tq *TransactionQueue) TransactionStep {
230 return func() error {
231 err := docker.StopContainer(tq.OldContainerID)
232 if err != nil {
233 return fmt.Errorf("stopping old container failed: %w", err)
234 }
235 return nil
236 }
237}
238
239// RemoveOldContainerStep removes the old container
240func RemoveOldContainerStep(tq *TransactionQueue) TransactionStep {
241 return func() error {
242 err := docker.RemoveContainer(tq.OldContainerID)
243 if err != nil {
244 return fmt.Errorf("removing old container failed: %w", err)
245 }
246 return nil
247 }
248}
249
250// RemoveNewContainerRollback removes the new container if the transaction fails
251func RemoveNewContainerRollback(tq *TransactionQueue) RollbackStep {
252 return func() error {
253 err := docker.RemoveContainer(tq.NewContainerID)
254 if err != nil {
255 return fmt.Errorf("rollback: removing new container failed: %w", err)
256 }
257 return nil
258 }
259}
260
261// StopNewContainerRollback stops the new container if the transaction fails
262func StopNewContainerRollback(tq *TransactionQueue) RollbackStep {
263 return func() error {
264 err := docker.StopContainer(tq.NewContainerID)
265 if err != nil {
266 return fmt.Errorf("rollback: stopping new container failed: %w", err)
267 }
268 return nil
269 }
270}
271
272// StartOldContainerRollback starts the old container if the transaction fails
273func StartOldContainerRollback(tq *TransactionQueue) RollbackStep {
274 return func() error {
275 err := docker.StartContainer(tq.OldContainerID)
276 if err != nil {
277 return fmt.Errorf("rollback: starting old container failed: %w", err)
278 }
279 return nil
280 }
281}
282
283// RenameNewContainerStep renames the new temporary container to the original name
284func RenameNewContainerStep(tq *TransactionQueue) TransactionStep {
285 return func() error {
286 err := docker.RenameContainer(tq.NewContainerID, tq.NewContainerName)
287 if err != nil {
288 return fmt.Errorf("renaming new container failed: %w", err)
289 }
290 return nil
291 }
292}
293
294// RenameNewContainerRollback renames the new container back to the temporary name if it fails
295func RenameNewContainerRollback(tq *TransactionQueue) RollbackStep {
296 return func() error {
297 err := docker.RenameContainer(tq.NewContainerID, tq.TempContainerName)
298 if err != nil {
299 return fmt.Errorf("rollback: renaming new container back failed: %w", err)
300 }
301 return nil
302 }
303}