1package handler
2
3import (
4 "fmt"
5 "net/http"
6 "strings"
7 "sync"
8 "time"
9
10 "github.com/bnema/gordon/internal/server"
11 "github.com/bnema/gordon/internal/templating/load"
12 "github.com/bnema/gordon/internal/templating/render"
13 "github.com/bnema/gordon/pkg/docker"
14 "github.com/bnema/gordon/pkg/humanize"
15 "github.com/docker/docker/api/types"
16 "github.com/labstack/echo/v4"
17)
18
19type MapOperation string
20
21var (
22 idMap = make(map[string]string)
23 idMapMutex sync.Mutex
24)
25
26const (
27 Fetch MapOperation = "Fetch"
28 Update MapOperation = "Update"
29 Delete MapOperation = "Delete"
30)
31
32func safelyInteractWithIDMap(op MapOperation, key string, value ...string) (string, bool) {
33 idMapMutex.Lock()
34 defer idMapMutex.Unlock()
35
36 switch op {
37 case Fetch:
38 val, exists := idMap[key]
39 return val, exists
40 case Update:
41 if len(value) > 0 {
42 idMap[key] = value[0]
43 }
44 return "", true
45 case Delete:
46 delete(idMap, key)
47 return "", true
48 default:
49 return "", false
50 }
51}
52
53type HumanReadableContainerImage struct {
54 ID string
55 Name string
56 ShortID string
57 CreatedStr string
58 SizeStr string
59 RepoDigests []string
60 RepoTags []string
61}
62
63type HumanReadableContainer struct {
64 *types.Container
65 Name string
66 Ports []string
67 ShortID string
68 CreatedStr string
69 SizeStr string
70 UpSince string
71 StateColor string
72 TraefikEntryPoint string
73}
74
75// renderHTML is a generalized function to render HTML
76func renderHTML(c echo.Context, a *server.App, path, templateName string, data map[string]interface{}) error {
77 rendererData, err := render.GetHTMLRenderer(path, templateName, a.TemplateFS, a)
78 if err != nil {
79 return err
80 }
81 renderedHTML, err := rendererData.Render(data, a)
82 if err != nil {
83 return err
84 }
85 return c.HTML(http.StatusOK, renderedHTML)
86}
87
88// ActionSuccess returns the success HTML fragment
89func ActionSuccess(a *server.App) string {
90 successFragment, err := load.Fragment(a, "success")
91 if err != nil {
92 fmt.Println(err)
93 return ""
94 }
95
96 return successFragment
97}
98
99// ImageManagerComponent handles the /image-manager route (HTMX route)
100func ImageManagerComponent(c echo.Context, a *server.App) error {
101 images, err := docker.ListContainerImages()
102 if err != nil {
103 return sendError(c, err)
104 }
105
106 var humanReadableImages []HumanReadableContainerImage
107
108 for _, img := range images {
109 shortID := img.ID[7:19]
110 safelyInteractWithIDMap(Update, shortID, img.ID)
111 createdTime := time.Unix(img.Created, 0)
112 createdStr := humanize.TimeAgo(createdTime)
113 sizeStr := humanize.BytesToReadableSize(img.Size)
114
115 for _, repoTag := range img.RepoTags {
116 humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
117 ID: img.ID,
118 ShortID: shortID,
119 CreatedStr: createdStr,
120 SizeStr: sizeStr,
121 Name: repoTag,
122 RepoDigests: img.RepoDigests,
123 RepoTags: img.RepoTags,
124 })
125 }
126 }
127
128 data := map[string]interface{}{
129 "Images": humanReadableImages,
130 }
131
132 return renderHTML(c, a, "html/fragments", "imagelist.gohtml", data)
133}
134
135// ImageManagerDelete handles the /image-manager/delete route
136func ImageManagerDelete(c echo.Context, a *server.App) error {
137 ShortID := c.Param("ID")
138
139 imageID, exists := safelyInteractWithIDMap(Fetch, ShortID)
140 if !exists {
141 return c.String(http.StatusBadRequest, "Invalid ShortImgID")
142 }
143
144 err := docker.DeleteContainerImage(imageID)
145 if err != nil {
146 c.Response().Header().Set("X-Error-Type", "image")
147 return c.String(http.StatusInternalServerError, err.Error())
148 }
149
150 safelyInteractWithIDMap(Delete, ShortID)
151
152 return c.String(http.StatusOK, ActionSuccess(a))
153}
154
155// ContainerManagerComponent handles the /container-manager route
156func ContainerManagerComponent(c echo.Context, a *server.App) error {
157 containers, err := docker.ListRunningContainers()
158 if err != nil {
159 return sendError(c, err)
160 }
161
162 var humanReadableContainers []HumanReadableContainer
163
164 for _, container := range containers {
165 localContainer := container // Make a local copy
166 sizeStr := humanize.BytesToReadableSize(container.SizeRw)
167 stateColor := "blue"
168
169 if container.State == "running" {
170 stateColor = "green"
171 } else if container.State == "exited" {
172 stateColor = "red"
173 }
174 var ports []string
175 for _, port := range container.Ports {
176 portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort)
177 ports = append(ports, portStr)
178 }
179
180 for _, name := range container.Names {
181 name = name[1:]
182 humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{
183 Container: &localContainer,
184 SizeStr: sizeStr,
185 UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),
186 StateColor: stateColor,
187 Name: name,
188 Ports: ports,
189 TraefikEntryPoint: extractTraefikEntryPoint(container.Labels),
190 CreatedStr: humanize.TimeAgo(time.Unix(container.Created, 0)),
191 })
192 }
193 }
194 yamlData, err := GetLocalizedData(c, a)
195 if err != nil {
196 return err
197 }
198 data := map[string]interface{}{
199 "Lang": yamlData["Lang"],
200 "containers": humanReadableContainers,
201 }
202
203 return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data)
204}
205
206func extractTraefikEntryPoint(labels map[string]string) string {
207 for key, value := range labels {
208 if strings.HasSuffix(key, ".loadbalancer.server.port") {
209 return value
210 }
211 }
212 return ""
213}
214
215// ContainerManagerDelete handles the /container-manager/delete route
216func ContainerManagerDelete(c echo.Context, a *server.App) error {
217 err := docker.RemoveContainer(c.Param("ID"))
218 if err != nil {
219 return sendError(c, err)
220 }
221 return c.HTML(http.StatusOK, ActionSuccess(a))
222}
223
224// ContainerManagerStop handles the /container-manager/stop route
225func ContainerManagerStop(c echo.Context, a *server.App) error {
226 // Stop the container gracefully with a timeout
227 stopped, err := docker.StopContainerGracefully(c.Param("ID"), 3*time.Second)
228 if err != nil {
229 return sendError(c, err)
230 }
231 // if stopped is true, the container was stopped gracefully
232 if stopped {
233 return c.HTML(http.StatusOK, ActionSuccess(a))
234 }
235 // If stopped is false, the container was not stopped gracefully we force stop it
236 err = docker.StopContainerRagefully(c.Param("ID"))
237 if err != nil {
238 return sendError(c, err)
239 }
240
241 return c.HTML(http.StatusOK, ActionSuccess(a))
242}
243
244// ContainerManagerStart handles the /container-manager/start route
245func ContainerManagerStart(c echo.Context, a *server.App) error {
246 err := docker.StartContainer(c.Param("ID"))
247 if err != nil {
248 return sendError(c, err)
249 }
250 return c.HTML(http.StatusOK, ActionSuccess(a))
251}