v0.1.12 71a4995

containers.go RawHistoryFolder

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}

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