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