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