0.1.9 85724f1

containers.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close