v2.30.0-pre2 f03ab25

controlplane_local.go RawHistoryFolder

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

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