v2.16.0 ebd2ab3

controlplane_local.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"fmt"
6	"time"
7
8	"github.com/bnema/gordon/internal/adapters/dto"
9	"github.com/bnema/gordon/internal/adapters/in/cli/remote"
10	"github.com/bnema/gordon/internal/app"
11	"github.com/bnema/gordon/internal/boundaries/in"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15type localControlPlane struct {
16	configSvc    in.ConfigService
17	secretSvc    in.SecretService
18	containerSvc in.ContainerService
19	backupSvc    in.BackupService
20	registrySvc  in.RegistryService
21	healthSvc    in.HealthService
22	logSvc       in.LogService
23}
24
25func NewLocalControlPlane(kernel *app.Kernel) ControlPlane {
26	if kernel == nil {
27		return &localControlPlane{}
28	}
29
30	return &localControlPlane{
31		configSvc:    kernel.Config(),
32		secretSvc:    kernel.Secrets(),
33		containerSvc: kernel.Container(),
34		backupSvc:    kernel.Backup(),
35		registrySvc:  kernel.Registry(),
36		healthSvc:    kernel.Health(),
37		logSvc:       kernel.Logs(),
38	}
39}
40
41func (l *localControlPlane) ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error) {
42	if l.containerSvc != nil {
43		return toRemoteRouteInfos(l.containerSvc.ListRoutesWithDetails(ctx)), nil
44	}
45
46	if l.configSvc == nil {
47		return nil, fmt.Errorf("local config service unavailable")
48	}
49
50	routes := l.configSvc.GetRoutes(ctx)
51	infos := make([]remote.RouteInfo, 0, len(routes))
52	for _, route := range routes {
53		infos = append(infos, remote.RouteInfo{Domain: route.Domain, Image: route.Image})
54	}
55	return infos, nil
56}
57
58func (l *localControlPlane) GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) {
59	if l.healthSvc == nil {
60		return map[string]*remote.RouteHealth{}, nil
61	}
62
63	health := l.healthSvc.CheckAllRoutes(ctx)
64	result := make(map[string]*remote.RouteHealth, len(health))
65	for domainName, h := range health {
66		if h == nil {
67			continue
68		}
69		result[domainName] = &remote.RouteHealth{
70			ContainerStatus: h.ContainerStatus,
71			HTTPStatus:      h.HTTPStatus,
72			ResponseTimeMs:  h.ResponseTimeMs,
73			Healthy:         h.Healthy,
74			Error:           h.Error,
75		}
76	}
77	return result, nil
78}
79
80func (l *localControlPlane) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) {
81	if l.configSvc == nil {
82		return nil, fmt.Errorf("local config service unavailable")
83	}
84	return l.configSvc.GetRoute(ctx, routeDomain)
85}
86
87func (l *localControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
88	if l.configSvc == nil {
89		return nil, fmt.Errorf("local config service unavailable")
90	}
91	return l.configSvc.FindRoutesByImage(ctx, imageName), nil
92}
93
94func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
95	if l.configSvc == nil {
96		return fmt.Errorf("local config service unavailable")
97	}
98	return l.configSvc.AddRoute(ctx, route)
99}
100
101func (l *localControlPlane) UpdateRoute(ctx context.Context, route domain.Route) error {
102	if l.configSvc == nil {
103		return fmt.Errorf("local config service unavailable")
104	}
105	return l.configSvc.UpdateRoute(ctx, route)
106}
107
108func (l *localControlPlane) RemoveRoute(ctx context.Context, routeDomain string) error {
109	if l.configSvc == nil {
110		return fmt.Errorf("local config service unavailable")
111	}
112	return l.configSvc.RemoveRoute(ctx, routeDomain)
113}
114
115func (l *localControlPlane) ListSecretsWithAttachments(ctx context.Context, secretDomain string) (*remote.SecretsListResult, error) {
116	if l.secretSvc == nil {
117		return nil, fmt.Errorf("local secret service unavailable")
118	}
119
120	keys, attachments, err := l.secretSvc.ListKeysWithAttachments(ctx, secretDomain)
121	if err != nil {
122		return nil, err
123	}
124
125	result := &remote.SecretsListResult{
126		Domain: secretDomain,
127		Keys:   keys,
128	}
129	for _, att := range attachments {
130		result.Attachments = append(result.Attachments, remote.AttachmentSecrets{
131			Service: att.Service,
132			Keys:    att.Keys,
133		})
134	}
135
136	return result, nil
137}
138
139func (l *localControlPlane) SetSecrets(ctx context.Context, secretDomain string, secrets map[string]string) error {
140	if l.secretSvc == nil {
141		return fmt.Errorf("local secret service unavailable")
142	}
143	return l.secretSvc.Set(ctx, secretDomain, secrets)
144}
145
146func (l *localControlPlane) DeleteSecret(ctx context.Context, secretDomain, key string) error {
147	if l.secretSvc == nil {
148		return fmt.Errorf("local secret service unavailable")
149	}
150	return l.secretSvc.Delete(ctx, secretDomain, key)
151}
152
153func (l *localControlPlane) SetAttachmentSecrets(ctx context.Context, domainName, service string, secrets map[string]string) error {
154	if l.secretSvc == nil {
155		return fmt.Errorf("local secret service unavailable")
156	}
157	return l.secretSvc.SetAttachment(ctx, domainName, service, secrets)
158}
159
160func (l *localControlPlane) DeleteAttachmentSecret(ctx context.Context, domainName, service, key string) error {
161	if l.secretSvc == nil {
162		return fmt.Errorf("local secret service unavailable")
163	}
164	return l.secretSvc.DeleteAttachment(ctx, domainName, service, key)
165}
166
167func (l *localControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
168	if l.configSvc == nil {
169		return nil, fmt.Errorf("local config service unavailable")
170	}
171	return l.configSvc.GetAllAttachments(ctx), nil
172}
173
174func (l *localControlPlane) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) {
175	if l.configSvc == nil {
176		return nil, fmt.Errorf("local config service unavailable")
177	}
178	return l.configSvc.GetAttachmentsFor(ctx, domainOrGroup)
179}
180
181func (l *localControlPlane) AddAttachment(ctx context.Context, domainOrGroup, image string) error {
182	if l.configSvc == nil {
183		return fmt.Errorf("local config service unavailable")
184	}
185	return l.configSvc.AddAttachment(ctx, domainOrGroup, image)
186}
187
188func (l *localControlPlane) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error {
189	if l.configSvc == nil {
190		return fmt.Errorf("local config service unavailable")
191	}
192	return l.configSvc.RemoveAttachment(ctx, domainOrGroup, image)
193}
194
195func (l *localControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
196	if l.configSvc == nil {
197		return nil, fmt.Errorf("local config service unavailable")
198	}
199
200	status := &remote.Status{
201		Routes:           len(l.configSvc.GetRoutes(ctx)),
202		RegistryDomain:   l.configSvc.GetRegistryDomain(),
203		RegistryPort:     l.configSvc.GetRegistryPort(),
204		ServerPort:       l.configSvc.GetServerPort(),
205		AutoRoute:        l.configSvc.IsAutoRouteEnabled(),
206		NetworkIsolation: l.configSvc.IsNetworkIsolationEnabled(),
207		ContainerStatus:  map[string]string{},
208	}
209
210	if l.containerSvc != nil {
211		for domainName, container := range l.containerSvc.List(ctx) {
212			if container == nil {
213				continue
214			}
215			status.ContainerStatus[domainName] = container.Status
216		}
217	}
218
219	return status, nil
220}
221
222func (l *localControlPlane) Reload(_ context.Context) error {
223	return app.SendReloadSignal()
224}
225
226func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
227	if l.containerSvc != nil && l.configSvc != nil {
228		route, err := l.configSvc.GetRoute(ctx, deployDomain)
229		if err != nil {
230			return nil, err
231		}
232		container, err := l.containerSvc.Deploy(ctx, *route)
233		if err != nil {
234			return nil, err
235		}
236		result := &remote.DeployResult{Status: "deployed", Domain: deployDomain}
237		if container != nil {
238			result.ContainerID = container.ID
239		}
240		return result, nil
241	}
242
243	domainName, err := app.SendDeploySignal(deployDomain)
244	if err != nil {
245		return nil, err
246	}
247	return &remote.DeployResult{Status: "queued", Domain: domainName}, nil
248}
249
250func (l *localControlPlane) Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) {
251	if l.containerSvc == nil {
252		if withAttachments {
253			return nil, fmt.Errorf("local restart with attachments requires active local container service")
254		}
255		domainName, err := app.SendDeploySignal(restartDomain)
256		if err != nil {
257			return nil, err
258		}
259		return &remote.RestartResult{Status: "queued", Domain: domainName}, nil
260	}
261
262	if err := l.containerSvc.SyncContainers(ctx); err != nil {
263		return nil, err
264	}
265	if err := l.containerSvc.Restart(ctx, restartDomain, withAttachments); err != nil {
266		return nil, err
267	}
268
269	return &remote.RestartResult{Status: "restarted", Domain: restartDomain}, nil
270}
271
272func (l *localControlPlane) ListTags(ctx context.Context, repository string) ([]string, error) {
273	if l.registrySvc == nil {
274		return nil, fmt.Errorf("local registry service unavailable")
275	}
276	return l.registrySvc.ListTags(ctx, repository)
277}
278
279func (l *localControlPlane) ListBackups(ctx context.Context, backupDomain string) ([]dto.BackupJob, error) {
280	if l.backupSvc == nil {
281		return nil, fmt.Errorf("local backup service unavailable")
282	}
283	jobs, err := l.backupSvc.ListBackups(ctx, backupDomain)
284	if err != nil {
285		return nil, err
286	}
287	return toDTOBackupJobs(jobs), nil
288}
289
290func (l *localControlPlane) BackupStatus(ctx context.Context) ([]dto.BackupJob, error) {
291	if l.backupSvc == nil {
292		return nil, fmt.Errorf("local backup service unavailable")
293	}
294	jobs, err := l.backupSvc.Status(ctx)
295	if err != nil {
296		return nil, err
297	}
298	return toDTOBackupJobs(jobs), nil
299}
300
301func (l *localControlPlane) RunBackup(ctx context.Context, backupDomain, dbName string) (*dto.BackupRunResponse, error) {
302	if l.backupSvc == nil {
303		return nil, fmt.Errorf("local backup service unavailable")
304	}
305	result, err := l.backupSvc.RunBackup(ctx, backupDomain, dbName)
306	if err != nil {
307		return nil, err
308	}
309	if result == nil {
310		return &dto.BackupRunResponse{Status: "ok"}, nil
311	}
312	job := toDTOBackupJob(result.Job)
313	return &dto.BackupRunResponse{Status: "ok", Backup: &job}, nil
314}
315
316func (l *localControlPlane) DetectDatabases(ctx context.Context, backupDomain string) ([]dto.DatabaseInfo, error) {
317	if l.backupSvc == nil {
318		return nil, fmt.Errorf("local backup service unavailable")
319	}
320	dbs, err := l.backupSvc.DetectDatabases(ctx, backupDomain)
321	if err != nil {
322		return nil, err
323	}
324	out := make([]dto.DatabaseInfo, 0, len(dbs))
325	for _, db := range dbs {
326		out = append(out, dto.DatabaseInfo{
327			Type:        string(db.Type),
328			Name:        db.Name,
329			Version:     db.Version,
330			Host:        db.Host,
331			Port:        db.Port,
332			ContainerID: db.ContainerID,
333			ImageName:   db.ImageName,
334		})
335	}
336	return out, nil
337}
338
339func (l *localControlPlane) GetProcessLogs(ctx context.Context, lines int) ([]string, error) {
340	if l.logSvc == nil {
341		return nil, fmt.Errorf("local log service unavailable")
342	}
343	return l.logSvc.GetProcessLogs(ctx, lines)
344}
345
346func (l *localControlPlane) GetContainerLogs(ctx context.Context, logDomain string, lines int) ([]string, error) {
347	if l.logSvc == nil {
348		return nil, fmt.Errorf("local log service unavailable")
349	}
350	return l.logSvc.GetContainerLogs(ctx, logDomain, lines)
351}
352
353func (l *localControlPlane) StreamProcessLogs(ctx context.Context, lines int) (<-chan string, error) {
354	if l.logSvc == nil {
355		return nil, fmt.Errorf("local log service unavailable")
356	}
357	return l.logSvc.FollowProcessLogs(ctx, lines)
358}
359
360func (l *localControlPlane) StreamContainerLogs(ctx context.Context, logDomain string, lines int) (<-chan string, error) {
361	if l.logSvc == nil {
362		return nil, fmt.Errorf("local log service unavailable")
363	}
364	return l.logSvc.FollowContainerLogs(ctx, logDomain, lines)
365}
366
367func toRemoteRouteInfos(routes []domain.RouteInfo) []remote.RouteInfo {
368	out := make([]remote.RouteInfo, 0, len(routes))
369	for _, route := range routes {
370		attachments := make([]dto.Attachment, 0, len(route.Attachments))
371		for _, attachment := range route.Attachments {
372			attachments = append(attachments, dto.Attachment{
373				Name:        attachment.Name,
374				Image:       attachment.Image,
375				ContainerID: attachment.ContainerID,
376				Status:      attachment.Status,
377				Network:     attachment.Network,
378			})
379		}
380
381		out = append(out, remote.RouteInfo{
382			Domain:          route.Domain,
383			Image:           route.Image,
384			ContainerID:     route.ContainerID,
385			ContainerStatus: route.ContainerStatus,
386			Network:         route.Network,
387			Attachments:     attachments,
388		})
389	}
390	return out
391}
392
393func toDTOBackupJobs(jobs []domain.BackupJob) []dto.BackupJob {
394	out := make([]dto.BackupJob, 0, len(jobs))
395	for _, job := range jobs {
396		out = append(out, toDTOBackupJob(job))
397	}
398	return out
399}
400
401func toDTOBackupJob(job domain.BackupJob) dto.BackupJob {
402	var startedAt *time.Time
403	if !job.StartedAt.IsZero() {
404		t := job.StartedAt
405		startedAt = &t
406	}
407	var completedAt *time.Time
408	if !job.CompletedAt.IsZero() {
409		t := job.CompletedAt
410		completedAt = &t
411	}
412
413	return dto.BackupJob{
414		ID:          job.ID,
415		Domain:      job.Domain,
416		DBName:      job.DBName,
417		Schedule:    string(job.Schedule),
418		Type:        string(job.Type),
419		Status:      string(job.Status),
420		StartedAt:   startedAt,
421		CompletedAt: completedAt,
422		SizeBytes:   job.SizeBytes,
423		Error:       job.Error,
424	}
425}

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 8

Tags 106

↑↓ moveenter openesc close