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 *types.ImageSummary
54 Name string
55 ShortID string
56 CreatedStr string
57 SizeStr string
58}
59
60type HumanReadableContainer struct {
61 *types.Container
62 Name string
63 Ports []string
64 ShortID string
65 CreatedStr string
66 SizeStr string
67 UpSince string
68 StateColor string
69}
70
71// renderHTML is a generalized function to render HTML
72func renderHTML(c echo.Context, a *server.App, path, templateName string, data map[string]interface{}) error {
73 rendererData, err := render.GetHTMLRenderer(path, templateName, a.TemplateFS, a)
74 if err != nil {
75 return err
76 }
77 renderedHTML, err := rendererData.Render(data, a)
78 if err != nil {
79 return err
80 }
81 return c.HTML(http.StatusOK, renderedHTML)
82}
83
84// ActionSuccess returns the success HTML fragment
85func ActionSuccess(a *server.App) string {
86 successFragment, err := load.Fragment(a, "success")
87 if err != nil {
88 fmt.Println(err)
89 return ""
90 }
91
92 return successFragment
93}
94
95// ImageManagerComponent handles the /image-manager route (HTMX route)
96func ImageManagerComponent(c echo.Context, a *server.App) error {
97 images, err := docker.ListContainerImages()
98 if err != nil {
99 return sendError(c, err)
100 }
101 var humanReadableImages []HumanReadableContainerImage
102
103 for _, image := range images {
104 // We need to shorten the ID because it breaks the HTML
105 ShortID := image.ID[7:19]
106 // And we store both the ShortID and the full ID in a map so we can retrieve the full ID later
107 safelyInteractWithIDMap(Update, ShortID, image.ID)
108 createdTime := time.Unix(image.Created, 0)
109 createdStr := humanize.TimeAgo(createdTime)
110 sizeStr := humanize.BytesToReadableSize(image.Size)
111 for _, repoTag := range image.RepoTags {
112 humanReadableImages = append(humanReadableImages, HumanReadableContainerImage{
113 ImageSummary: &types.ImageSummary{
114 ID: ShortID, // Set the ID to the ShortID
115 },
116 CreatedStr: createdStr,
117 SizeStr: sizeStr,
118 Name: repoTag,
119 })
120 }
121 }
122 data := map[string]interface{}{
123 "Images": humanReadableImages,
124 }
125
126 return renderHTML(c, a, "html/fragments", "imagelist.gohtml", data)
127}
128
129// ImageManagerDelete handles the /image-manager/delete route
130func ImageManagerDelete(c echo.Context, a *server.App) error {
131 //
132 ShortID := c.Param("ID")
133
134 imageID, exists := safelyInteractWithIDMap(Fetch, ShortID)
135
136 if !exists {
137 return c.String(http.StatusBadRequest, "Invalid ShortImgID")
138 }
139
140 err := docker.DeleteContainerImage(imageID)
141 if err != nil {
142 c.Response().Header().Set("X-Error-Type", "image")
143 return c.String(http.StatusInternalServerError, err.Error())
144 }
145
146 safelyInteractWithIDMap(Delete, ShortID)
147
148 return c.String(http.StatusOK, ActionSuccess(a))
149}
150
151// ContainerManagerComponent handles the /container-manager route
152func ContainerManagerComponent(c echo.Context, a *server.App) error {
153 containers, err := docker.ListRunningContainers()
154 if err != nil {
155 return sendError(c, err)
156 }
157
158 var humanReadableContainers []HumanReadableContainer
159
160 for _, container := range containers {
161 localContainer := container // Make a local copy
162 sizeStr := humanize.BytesToReadableSize(container.SizeRw)
163 stateColor := "green"
164
165 if container.State == "exited" {
166 stateColor = "red"
167 }
168 var ports []string
169 for _, port := range container.Ports {
170 portStr := fmt.Sprintf("%d:%d", port.PublicPort, port.PrivatePort)
171 ports = append(ports, portStr)
172 }
173
174 for _, name := range container.Names {
175 name = name[1:]
176 humanReadableContainers = append(humanReadableContainers, HumanReadableContainer{
177 Container: &localContainer,
178 SizeStr: sizeStr,
179 UpSince: humanize.TimeAgo(time.Unix(container.Created, 0)),
180 StateColor: stateColor,
181 Name: name,
182 Ports: ports,
183 })
184 }
185 }
186 yamlData, err := GetLocalizedData(c, a)
187 if err != nil {
188 return err
189 }
190 data := map[string]interface{}{
191 "Lang": yamlData["Lang"],
192 "containers": humanReadableContainers,
193 }
194
195 return renderHTML(c, a, "html/fragments", "containerlist.gohtml", data)
196}
197
198// ContainerManagerDelete handles the /container-manager/delete route
199func ContainerManagerDelete(c echo.Context, a *server.App) error {
200 err := docker.RemoveContainer(c.Param("ID"))
201 if err != nil {
202 return sendError(c, err)
203 }
204 return c.HTML(http.StatusOK, ActionSuccess(a))
205}
206
207// ContainerManagerStop handles the /container-manager/stop route
208func ContainerManagerStop(c echo.Context, a *server.App) error {
209 // Stop the container gracefully with a timeout
210 stopped, err := docker.StopContainerGracefully(c.Param("ID"), 3*time.Second)
211 if err != nil {
212 return sendError(c, err)
213 }
214 // if stopped is true, the container was stopped gracefully
215 if stopped {
216 return c.HTML(http.StatusOK, ActionSuccess(a))
217 }
218 // If stopped is false, the container was not stopped gracefully we force stop it
219 err = docker.StopContainerRagefully(c.Param("ID"))
220 if err != nil {
221 return sendError(c, err)
222 }
223
224 return c.HTML(http.StatusOK, ActionSuccess(a))
225}
226
227// ContainerManagerStart handles the /container-manager/start route
228func ContainerManagerStart(c echo.Context, a *server.App) error {
229 err := docker.StartContainer(c.Param("ID"))
230 if err != nil {
231 return sendError(c, err)
232 }
233 return c.HTML(http.StatusOK, ActionSuccess(a))
234}