v2.31.0-dev19 35467d7

controlplane_local.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"sort"
8	"time"
9
10	"github.com/bnema/gordon/internal/adapters/dto"
11	"github.com/bnema/gordon/internal/adapters/in/cli/remote"
12	"github.com/bnema/gordon/internal/app"
13	"github.com/bnema/gordon/internal/boundaries/in"
14	"github.com/bnema/gordon/internal/domain"
15	"github.com/bnema/gordon/internal/usecase/config"
16)
17
18type localControlPlane struct {
19	configSvc    in.ConfigService
20	secretSvc    in.SecretService
21	containerSvc in.ContainerService
22	backupSvc    in.BackupService
23	registrySvc  in.RegistryService
24	deployCoord  in.DeployCoordinator
25	healthSvc    in.HealthService
26	logSvc       in.LogService
27	volumeSvc    in.VolumeService
28	publicTLSSvc in.PublicTLSService
29}
30
31func NewLocalControlPlane(kernel *app.Kernel) ControlPlane {
32	if kernel == nil {
33		return &localControlPlane{}
34	}
35
36	registrySvc := kernel.Registry()
37	var deployCoord in.DeployCoordinator
38	if registrySvc != nil {
39		if coordinator, ok := any(registrySvc).(in.DeployCoordinator); ok {
40			deployCoord = coordinator
41		}
42	}
43
44	return &localControlPlane{
45		configSvc:    kernel.Config(),
46		secretSvc:    kernel.Secrets(),
47		containerSvc: kernel.Container(),
48		backupSvc:    kernel.Backup(),
49		registrySvc:  registrySvc,
50		deployCoord:  deployCoord,
51		healthSvc:    kernel.Health(),
52		logSvc:       kernel.Logs(),
53		volumeSvc:    kernel.Volumes(),
54		publicTLSSvc: kernel.PublicTLS(),
55	}
56}
57
58func (l *localControlPlane) ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error) {
59	if l.containerSvc != nil {
60		if err := l.containerSvc.SyncContainers(ctx); err != nil {
61			return nil, err
62		}
63		detailed := l.containerSvc.ListRoutesWithDetails(ctx)
64		if l.configSvc == nil {
65			return toRemoteRouteInfos(detailed), nil
66		}
67		return mergeConfiguredRemoteRouteInfos(l.configSvc.GetRoutes(ctx), detailed), nil
68	}
69
70	if l.configSvc == nil {
71		return nil, fmt.Errorf("local config service unavailable")
72	}
73
74	routes := l.configSvc.GetRoutes(ctx)
75	infos := make([]remote.RouteInfo, 0, len(routes))
76	for _, route := range routes {
77		infos = append(infos, remote.RouteInfo{Domain: route.Domain, Image: route.Image})
78	}
79	return infos, nil
80}
81
82func (l *localControlPlane) GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) {
83	if l.healthSvc == nil {
84		return map[string]*remote.RouteHealth{}, nil
85	}
86
87	health := l.healthSvc.CheckAllRoutes(ctx)
88	result := make(map[string]*remote.RouteHealth, len(health))
89	for domainName, h := range health {
90		if h == nil {
91			continue
92		}
93		result[domainName] = &remote.RouteHealth{
94			ContainerStatus: h.ContainerStatus,
95			HTTPStatus:      h.HTTPStatus,
96			ResponseTimeMs:  h.ResponseTimeMs,
97			Healthy:         h.Healthy,
98			Error:           h.Error,
99		}
100	}
101	return result, nil
102}
103
104func (l *localControlPlane) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) {
105	if l.configSvc == nil {
106		return nil, fmt.Errorf("local config service unavailable")
107	}
108	return l.configSvc.GetRoute(ctx, routeDomain)
109}
110
111func (l *localControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
112	if l.configSvc == nil {
113		return nil, fmt.Errorf("local config service unavailable")
114	}
115	return l.configSvc.FindRoutesByImage(ctx, imageName), nil
116}
117
118func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
119	if l.configSvc == nil {
120		return fmt.Errorf("local config service unavailable")
121	}
122	return l.configSvc.AddRoute(ctx, route)
123}
124
125func (l *localControlPlane) UpdateRoute(ctx context.Context, route domain.Route) error {
126	if l.configSvc == nil {
127		return fmt.Errorf("local config service unavailable")
128	}
129	return l.configSvc.UpdateRoute(ctx, route)
130}
131
132func (l *localControlPlane) RemoveRoute(ctx context.Context, routeDomain string) error {
133	_, err := l.RemoveRouteWithCleanup(ctx, routeDomain)
134	return err
135}
136
137func (l *localControlPlane) RemoveRouteWithCleanup(ctx context.Context, routeDomain string) (*dto.RouteDeleteResponse, error) {
138	if l.configSvc == nil {
139		return nil, fmt.Errorf("local config service unavailable")
140	}
141	if err := l.configSvc.RemoveRoute(ctx, routeDomain); err != nil && !errors.Is(err, domain.ErrRouteNotFound) {
142		return nil, fmt.Errorf("remove route: %w", err)
143	}
144	resp := &dto.RouteDeleteResponse{Status: "removed"}
145	if l.containerSvc == nil {
146		return resp, nil
147	}
148	report, err := l.containerSvc.ReconcileRemovedRoute(ctx, routeDomain)
149	if err != nil {
150		return nil, fmt.Errorf("failed to cleanup removed route runtime state: %w", err)
151	}
152	resp.Cleanup = dto.CleanupReportFromDomain(report)
153	return resp, nil
154}
155
156func (l *localControlPlane) Bootstrap(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
157	registryDomain := ""
158	if l.configSvc != nil {
159		registryDomain = l.configSvc.GetRegistryDomain()
160	}
161	normalizedImage, err := config.NormalizeBootstrapImage(req.Image, registryDomain)
162	if err != nil {
163		return nil, fmt.Errorf("invalid image: %w", err)
164	}
165
166	resp := &dto.BootstrapResponse{
167		Domain: req.Domain,
168		Image:  normalizedImage,
169		Next:   fmt.Sprintf("push %s to trigger deployment", normalizedImage),
170	}
171
172	if l.configSvc == nil {
173		return resp, fmt.Errorf("local config service unavailable")
174	}
175	needsSecrets := len(req.Env) > 0 || len(req.AttachmentEnv) > 0
176	if needsSecrets && l.secretSvc == nil {
177		return resp, fmt.Errorf("local secret service unavailable")
178	}
179
180	addStep := func(name, status string) {
181		resp.Steps = append(resp.Steps, dto.BootstrapStep{Name: name, Status: status})
182	}
183
184	err = l.configSvc.AddRoute(ctx, domain.Route{Domain: req.Domain, Image: normalizedImage})
185	switch err {
186	case nil:
187		addStep("route", "configured")
188	default:
189		addStep("route", "failed")
190		return resp, err
191	}
192
193	if err := l.bootstrapAttachments(ctx, req, addStep); err != nil {
194		return resp, err
195	}
196
197	if err := l.bootstrapSecrets(ctx, req, addStep); err != nil {
198		return resp, err
199	}
200
201	if err := l.bootstrapAttachmentSecrets(ctx, req, addStep); err != nil {
202		return resp, err
203	}
204
205	return resp, nil
206}
207
208func (l *localControlPlane) bootstrapAttachments(ctx context.Context, req dto.BootstrapRequest, addStep func(name, status string)) error {
209	for _, attachment := range req.Attachments {
210		err := l.configSvc.AddAttachment(ctx, req.Domain, attachment)
211		if err == nil {
212			addStep("attachment:"+attachment, "created")
213			continue
214		}
215		if errors.Is(err, domain.ErrAttachmentExists) {
216			addStep("attachment:"+attachment, "noop")
217			continue
218		}
219		addStep("attachment:"+attachment, "failed")
220		return err
221	}
222
223	return nil
224}
225
226func (l *localControlPlane) bootstrapSecrets(ctx context.Context, req dto.BootstrapRequest, addStep func(name, status string)) error {
227	if len(req.Env) == 0 {
228		return nil
229	}
230
231	if err := l.secretSvc.Set(ctx, req.Domain, req.Env); err != nil {
232		addStep("env", "failed")
233		return err
234	}
235
236	addStep("env", "updated")
237	return nil
238}
239
240func (l *localControlPlane) bootstrapAttachmentSecrets(ctx context.Context, req dto.BootstrapRequest, addStep func(name, status string)) error {
241	for service, env := range req.AttachmentEnv {
242		if err := l.secretSvc.SetAttachment(ctx, req.Domain, service, env); err != nil {
243			addStep("attachment_env:"+service, "failed")
244			return err
245		}
246		addStep("attachment_env:"+service, "updated")
247	}
248
249	return nil
250}
251
252func (l *localControlPlane) ListSecretsWithAttachments(ctx context.Context, secretDomain string) (*remote.SecretsListResult, error) {
253	if l.secretSvc == nil {
254		return nil, fmt.Errorf("local secret service unavailable")
255	}
256
257	keys, attachments, err := l.secretSvc.ListKeysWithAttachments(ctx, secretDomain)
258	if err != nil {
259		return nil, fmt.Errorf("list secret keys with attachments: %w", err)
260	}
261
262	result := &remote.SecretsListResult{
263		Domain: secretDomain,
264		Keys:   keys,
265	}
266	for _, att := range attachments {
267		result.Attachments = append(result.Attachments, remote.AttachmentSecrets{
268			Service: att.Service,
269			Keys:    att.Keys,
270		})
271	}
272
273	return result, nil
274}
275
276func (l *localControlPlane) SetSecrets(ctx context.Context, secretDomain string, secrets map[string]string) error {
277	if l.secretSvc == nil {
278		return fmt.Errorf("local secret service unavailable")
279	}
280	return l.secretSvc.Set(ctx, secretDomain, secrets)
281}
282
283func (l *localControlPlane) DeleteSecret(ctx context.Context, secretDomain, key string) error {
284	if l.secretSvc == nil {
285		return fmt.Errorf("local secret service unavailable")
286	}
287	return l.secretSvc.Delete(ctx, secretDomain, key)
288}
289
290func (l *localControlPlane) SetAttachmentSecrets(ctx context.Context, domainName, service string, secrets map[string]string) error {
291	if l.secretSvc == nil {
292		return fmt.Errorf("local secret service unavailable")
293	}
294	return l.secretSvc.SetAttachment(ctx, domainName, service, secrets)
295}
296
297func (l *localControlPlane) DeleteAttachmentSecret(ctx context.Context, domainName, service, key string) error {
298	if l.secretSvc == nil {
299		return fmt.Errorf("local secret service unavailable")
300	}
301	return l.secretSvc.DeleteAttachment(ctx, domainName, service, key)
302}
303
304func (l *localControlPlane) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
305	if l.containerSvc == nil {
306		return nil, fmt.Errorf("local container service unavailable")
307	}
308	attachments, err := l.containerSvc.ListOrphanedAttachments(ctx)
309	if err != nil {
310		return nil, fmt.Errorf("list orphaned attachments: %w", err)
311	}
312	return attachments, nil
313}
314
315func (l *localControlPlane) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
316	if l.containerSvc == nil {
317		return nil, fmt.Errorf("local container service unavailable")
318	}
319	report, err := l.containerSvc.CleanupOrphanedAttachments(ctx, owner, stop)
320	if err != nil {
321		return nil, fmt.Errorf("cleanup orphaned attachments: %w", err)
322	}
323	return report, nil
324}
325
326func (l *localControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
327	if l.configSvc == nil {
328		return nil, fmt.Errorf("local config service unavailable")
329	}
330	return l.configSvc.GetAllAttachments(ctx), nil
331}
332
333func (l *localControlPlane) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) {
334	if l.configSvc == nil {
335		return nil, fmt.Errorf("local config service unavailable")
336	}
337	return l.configSvc.GetAttachmentsFor(ctx, domainOrGroup)
338}
339
340func (l *localControlPlane) FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error) {
341	if l.configSvc == nil {
342		return nil, fmt.Errorf("local config service unavailable")
343	}
344	return l.configSvc.FindAttachmentTargetsByImage(ctx, imageName), nil
345}
346
347func (l *localControlPlane) AddAttachment(ctx context.Context, domainOrGroup, image string) error {
348	if l.configSvc == nil {
349		return fmt.Errorf("local config service unavailable")
350	}
351	return l.configSvc.AddAttachment(ctx, domainOrGroup, image)
352}
353
354func (l *localControlPlane) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error {
355	if l.configSvc == nil {
356		return fmt.Errorf("local config service unavailable")
357	}
358	return l.configSvc.RemoveAttachment(ctx, domainOrGroup, image)
359}
360
361func (l *localControlPlane) GetAutoRouteAllowedDomains(ctx context.Context) ([]string, error) {
362	if l.configSvc == nil {
363		return nil, fmt.Errorf("local config service unavailable")
364	}
365	return l.configSvc.GetAutoRouteAllowedDomains(ctx)
366}
367
368func (l *localControlPlane) AddAutoRouteAllowedDomain(ctx context.Context, pattern string) error {
369	if l.configSvc == nil {
370		return fmt.Errorf("local config service unavailable")
371	}
372	return l.configSvc.AddAutoRouteAllowedDomain(ctx, pattern)
373}
374
375func (l *localControlPlane) RemoveAutoRouteAllowedDomain(ctx context.Context, pattern string) error {
376	if l.configSvc == nil {
377		return fmt.Errorf("local config service unavailable")
378	}
379	return l.configSvc.RemoveAutoRouteAllowedDomain(ctx, pattern)
380}
381
382func (l *localControlPlane) GetTLSStatus(ctx context.Context) (*dto.TLSStatusResponse, error) {
383	if l.publicTLSSvc == nil {
384		return &dto.TLSStatusResponse{
385			ACMEEnabled:     false,
386			SelectionReason: "public TLS service not configured",
387		}, nil
388	}
389
390	status := l.publicTLSSvc.Status(ctx)
391	result := dto.TLSStatusFromDomain(status)
392	return &result, nil
393}
394
395func (l *localControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
396	if l.configSvc == nil {
397		return nil, fmt.Errorf("local config service unavailable")
398	}
399
400	status := &remote.Status{
401		Routes:           len(l.configSvc.GetRoutes(ctx)),
402		RegistryDomain:   l.configSvc.GetRegistryDomain(),
403		RegistryPort:     l.configSvc.GetRegistryPort(),
404		ServerPort:       l.configSvc.GetServerPort(),
405		AutoRoute:        l.configSvc.IsAutoRouteEnabled(),
406		NetworkIsolation: l.configSvc.IsNetworkIsolationEnabled(),
407		ContainerStatus:  map[string]string{},
408	}
409
410	if l.containerSvc != nil {
411		for domainName, container := range l.containerSvc.List(ctx) {
412			if container == nil {
413				continue
414			}
415			status.ContainerStatus[domainName] = container.Status
416		}
417	}
418
419	return status, nil
420}
421
422func (l *localControlPlane) Reload(_ context.Context) error {
423	return app.SendReloadSignal()
424}
425
426func (l *localControlPlane) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) {
427	if l.containerSvc == nil {
428		return nil, fmt.Errorf("container service unavailable")
429	}
430	return l.containerSvc.ListNetworks(ctx)
431}
432
433func (l *localControlPlane) GetConfig(ctx context.Context) (*remote.Config, error) {
434	if l.configSvc == nil {
435		return nil, fmt.Errorf("config service unavailable")
436	}
437	externalRoutes := l.configSvc.GetExternalRoutes()
438	externalResponses := make([]remote.ExternalRoute, 0, len(externalRoutes))
439	for domainName := range externalRoutes {
440		externalResponses = append(externalResponses, remote.ExternalRoute{Domain: domainName})
441	}
442	sort.Slice(externalResponses, func(i, j int) bool {
443		return externalResponses[i].Domain < externalResponses[j].Domain
444	})
445	cfg := &remote.Config{
446		Routes:         l.configSvc.GetRoutes(ctx),
447		ExternalRoutes: externalResponses,
448	}
449	cfg.Server.Port = l.configSvc.GetServerPort()
450	cfg.Server.RegistryPort = l.configSvc.GetRegistryPort()
451	cfg.Server.RegistryDomain = l.configSvc.GetRegistryDomain()
452	cfg.AutoRoute.Enabled = l.configSvc.IsAutoRouteEnabled()
453	cfg.NetworkIsolation.Enabled = l.configSvc.IsNetworkIsolationEnabled()
454	cfg.NetworkIsolation.Prefix = l.configSvc.GetNetworkPrefix()
455	if volumeCfg, ok := any(l.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok {
456		cfg.Volumes.AutoCreate, cfg.Volumes.Prefix, cfg.Volumes.Preserve = volumeCfg.GetVolumeConfig()
457	}
458	return cfg, nil
459}
460
461func (l *localControlPlane) DeployIntent(_ context.Context, imageName string) error {
462	if l.deployCoord != nil {
463		l.deployCoord.SuppressDeployEvent(imageName)
464	}
465	return nil
466}
467
468func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
469	if l.containerSvc != nil && l.configSvc != nil {
470		route, err := l.configSvc.GetRoute(ctx, deployDomain)
471		if err != nil {
472			return nil, err
473		}
474		container, err := l.containerSvc.Deploy(domain.WithInternalDeploy(ctx), *route)
475		if err != nil {
476			return nil, err
477		}
478		result := &remote.DeployResult{Status: "deployed", Domain: deployDomain}
479		if container != nil {
480			result.ContainerID = container.ID
481		}
482		return result, nil
483	}
484
485	domainName, err := app.SendDeploySignal(deployDomain)
486	if err != nil {
487		return nil, err
488	}
489	return &remote.DeployResult{Status: "queued", Domain: domainName}, nil
490}
491
492func (l *localControlPlane) Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) {
493	if l.containerSvc == nil {
494		if withAttachments {
495			return nil, fmt.Errorf("local restart with attachments requires active local container service")
496		}
497		domainName, err := app.SendDeploySignal(restartDomain)
498		if err != nil {
499			return nil, err
500		}
501		return &remote.RestartResult{Status: "queued", Domain: domainName}, nil
502	}
503
504	if err := l.containerSvc.SyncContainers(ctx); err != nil {
505		return nil, err
506	}
507	if err := l.containerSvc.Restart(ctx, restartDomain, withAttachments); err != nil {
508		return nil, err
509	}
510
511	return &remote.RestartResult{Status: "restarted", Domain: restartDomain}, nil
512}
513
514func (l *localControlPlane) ListTags(ctx context.Context, repository string) ([]string, error) {
515	if l.registrySvc == nil {
516		return nil, fmt.Errorf("local registry service unavailable")
517	}
518	return l.registrySvc.ListTags(ctx, repository)
519}
520
521func (l *localControlPlane) ListBackups(ctx context.Context, backupDomain string) ([]dto.BackupJob, error) {
522	if l.backupSvc == nil {
523		return nil, fmt.Errorf("local backup service unavailable")
524	}
525	jobs, err := l.backupSvc.ListBackups(ctx, backupDomain)
526	if err != nil {
527		return nil, err
528	}
529	return toDTOBackupJobs(jobs), nil
530}
531
532func (l *localControlPlane) BackupStatus(ctx context.Context) ([]dto.BackupJob, error) {
533	if l.backupSvc == nil {
534		return nil, fmt.Errorf("local backup service unavailable")
535	}
536	jobs, err := l.backupSvc.Status(ctx)
537	if err != nil {
538		return nil, err
539	}
540	return toDTOBackupJobs(jobs), nil
541}
542
543func (l *localControlPlane) RunBackup(ctx context.Context, backupDomain, dbName string) (*dto.BackupRunResponse, error) {
544	if l.backupSvc == nil {
545		return nil, fmt.Errorf("local backup service unavailable")
546	}
547	result, err := l.backupSvc.RunBackup(ctx, backupDomain, dbName)
548	if err != nil {
549		return nil, err
550	}
551	if result == nil {
552		return &dto.BackupRunResponse{Status: "ok"}, nil
553	}
554	job := toDTOBackupJob(result.Job)
555	return &dto.BackupRunResponse{Status: "ok", Backup: &job}, nil
556}
557
558func (l *localControlPlane) DetectDatabases(ctx context.Context, backupDomain string) ([]dto.DatabaseInfo, error) {
559	if l.backupSvc == nil {
560		return nil, fmt.Errorf("local backup service unavailable")
561	}
562	dbs, err := l.backupSvc.DetectDatabases(ctx, backupDomain)
563	if err != nil {
564		return nil, err
565	}
566	out := make([]dto.DatabaseInfo, 0, len(dbs))
567	for _, db := range dbs {
568		out = append(out, dto.DatabaseInfo{
569			Type:        string(db.Type),
570			Name:        db.Name,
571			Version:     db.Version,
572			Host:        db.Host,
573			Port:        db.Port,
574			ContainerID: db.ContainerID,
575			ImageName:   db.ImageName,
576		})
577	}
578	return out, nil
579}
580
581func (l *localControlPlane) GetProcessLogs(ctx context.Context, lines int) ([]string, error) {
582	if l.logSvc == nil {
583		return nil, fmt.Errorf("local log service unavailable")
584	}
585	return l.logSvc.GetProcessLogs(ctx, lines)
586}
587
588func (l *localControlPlane) GetContainerLogs(ctx context.Context, logDomain string, lines int) ([]string, error) {
589	if l.logSvc == nil {
590		return nil, fmt.Errorf("local log service unavailable")
591	}
592	return l.logSvc.GetContainerLogs(ctx, logDomain, lines)
593}
594
595func (l *localControlPlane) StreamProcessLogs(ctx context.Context, lines int) (<-chan string, error) {
596	if l.logSvc == nil {
597		return nil, fmt.Errorf("local log service unavailable")
598	}
599	return l.logSvc.FollowProcessLogs(ctx, lines)
600}
601
602func (l *localControlPlane) StreamContainerLogs(ctx context.Context, logDomain string, lines int) (<-chan string, error) {
603	if l.logSvc == nil {
604		return nil, fmt.Errorf("local log service unavailable")
605	}
606	return l.logSvc.FollowContainerLogs(ctx, logDomain, lines)
607}
608
609func (l *localControlPlane) ListVolumes(ctx context.Context) ([]dto.Volume, error) {
610	if l.volumeSvc == nil {
611		return nil, fmt.Errorf("volume service unavailable")
612	}
613	vols, err := l.volumeSvc.ListVolumes(ctx)
614	if err != nil {
615		return nil, err
616	}
617
618	result := make([]dto.Volume, len(vols))
619	for i, v := range vols {
620		result[i] = dto.Volume{
621			Name:       v.Name,
622			Driver:     v.Driver,
623			MountPoint: v.MountPoint,
624			Size:       v.Size,
625			CreatedAt:  v.CreatedAt,
626			InUse:      v.InUse,
627			Containers: v.Containers,
628			Labels:     v.Labels,
629		}
630	}
631	return result, nil
632}
633
634func (l *localControlPlane) PruneVolumes(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
635	if l.volumeSvc == nil {
636		return nil, fmt.Errorf("volume service unavailable")
637	}
638	report, removed, err := l.volumeSvc.PruneVolumes(ctx, req.DryRun)
639	if err != nil {
640		return nil, err
641	}
642
643	vols := make([]dto.Volume, len(removed))
644	for i, v := range removed {
645		vols[i] = dto.Volume{
646			Name: v.Name,
647			Size: v.Size,
648		}
649	}
650
651	return &dto.VolumePruneResponse{
652		VolumesRemoved: report.VolumesRemoved,
653		SpaceReclaimed: report.SpaceReclaimed,
654		Volumes:        vols,
655	}, nil
656}
657
658func mergeConfiguredRemoteRouteInfos(configured []domain.Route, detailed []domain.RouteInfo) []remote.RouteInfo {
659	detailedByDomain := make(map[string]domain.RouteInfo, len(detailed))
660	for _, info := range detailed {
661		detailedByDomain[info.Domain] = info
662	}
663
664	merged := make([]domain.RouteInfo, 0, len(configured))
665	for _, route := range configured {
666		info, ok := detailedByDomain[route.Domain]
667		if !ok {
668			info = domain.RouteInfo{Domain: route.Domain, Image: route.Image}
669		} else if info.Image == "" {
670			info.Image = route.Image
671		}
672		merged = append(merged, info)
673	}
674
675	return toRemoteRouteInfos(merged)
676}
677
678func toRemoteRouteInfos(routes []domain.RouteInfo) []remote.RouteInfo {
679	out := make([]remote.RouteInfo, 0, len(routes))
680	for _, route := range routes {
681		attachments := make([]dto.Attachment, 0, len(route.Attachments))
682		for _, attachment := range route.Attachments {
683			attachments = append(attachments, dto.Attachment{
684				Name:        attachment.Name,
685				Image:       attachment.Image,
686				ContainerID: attachment.ContainerID,
687				Status:      attachment.Status,
688				Network:     attachment.Network,
689			})
690		}
691
692		out = append(out, remote.RouteInfo{
693			Domain:          route.Domain,
694			Image:           route.Image,
695			ContainerID:     route.ContainerID,
696			ContainerStatus: route.ContainerStatus,
697			Network:         route.Network,
698			Attachments:     attachments,
699		})
700	}
701	return out
702}
703
704func toDTOBackupJobs(jobs []domain.BackupJob) []dto.BackupJob {
705	out := make([]dto.BackupJob, 0, len(jobs))
706	for _, job := range jobs {
707		out = append(out, toDTOBackupJob(job))
708	}
709	return out
710}
711
712func toDTOBackupJob(job domain.BackupJob) dto.BackupJob {
713	var startedAt *time.Time
714	if !job.StartedAt.IsZero() {
715		t := job.StartedAt
716		startedAt = &t
717	}
718	var completedAt *time.Time
719	if !job.CompletedAt.IsZero() {
720		t := job.CompletedAt
721		completedAt = &t
722	}
723
724	return dto.BackupJob{
725		ID:          job.ID,
726		Domain:      job.Domain,
727		DBName:      job.DBName,
728		Schedule:    string(job.Schedule),
729		Type:        string(job.Type),
730		Status:      string(job.Status),
731		StartedAt:   startedAt,
732		CompletedAt: completedAt,
733		SizeBytes:   job.SizeBytes,
734		Error:       job.Error,
735	}
736}

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