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) GetRouteCleanupPreview(ctx context.Context, routeDomain string) (*domain.CleanupReport, error) {
157 previewer, ok := any(l.containerSvc).(interface {
158 PreviewRemovedRouteCleanup(context.Context, string) (*domain.CleanupReport, error)
159 })
160 if !ok {
161 return nil, fmt.Errorf("route cleanup preview unavailable")
162 }
163
164 report, err := previewer.PreviewRemovedRouteCleanup(ctx, routeDomain)
165 if err != nil {
166 return nil, err
167 }
168 if report == nil {
169 return nil, nil
170 }
171 if l.volumeSvc == nil {
172 return report, nil
173 }
174
175 volumes, err := l.ListVolumes(ctx)
176 if err != nil {
177 return nil, fmt.Errorf("list volumes: %w", err)
178 }
179 if len(volumes) == 0 {
180 return report, nil
181 }
182
183 scope := newRouteResourceScope(routeDomain, l.routeCleanupVolumePrefix(), nil, report.PreservedAttachments)
184 for _, volume := range volumesForRouteDiagnosis(volumes, scope) {
185 report.PreservedVolumes = append(report.PreservedVolumes, domain.CleanupVolume{
186 Name: volume.Name,
187 Reason: "volume preserved for explicit cleanup review",
188 })
189 }
190 return report, nil
191}
192
193func (l *localControlPlane) routeCleanupVolumePrefix() string {
194 const defaultPrefix = "gordon"
195 if l.configSvc == nil {
196 return defaultPrefix
197 }
198 if volumeCfg, ok := any(l.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok {
199 _, prefix, _ := volumeCfg.GetVolumeConfig()
200 if prefix != "" {
201 return prefix
202 }
203 }
204 return defaultPrefix
205}
206
207func (l *localControlPlane) Bootstrap(ctx context.Context, req dto.BootstrapRequest) (*dto.BootstrapResponse, error) {
208 registryDomain := ""
209 if l.configSvc != nil {
210 registryDomain = l.configSvc.GetRegistryDomain()
211 }
212 normalizedImage, err := config.NormalizeBootstrapImage(req.Image, registryDomain)
213 if err != nil {
214 return nil, fmt.Errorf("invalid image: %w", err)
215 }
216
217 resp := &dto.BootstrapResponse{
218 Domain: req.Domain,
219 Image: normalizedImage,
220 Next: fmt.Sprintf("push %s to trigger deployment", normalizedImage),
221 }
222
223 if l.configSvc == nil {
224 return resp, fmt.Errorf("local config service unavailable")
225 }
226 needsSecrets := len(req.Env) > 0 || len(req.AttachmentEnv) > 0
227 if needsSecrets && l.secretSvc == nil {
228 return resp, fmt.Errorf("local secret service unavailable")
229 }
230
231 addStep := func(name, status string) {
232 resp.Steps = append(resp.Steps, dto.BootstrapStep{Name: name, Status: status})
233 }
234
235 err = l.configSvc.AddRoute(ctx, domain.Route{Domain: req.Domain, Image: normalizedImage})
236 switch err {
237 case nil:
238 addStep("route", "configured")
239 default:
240 addStep("route", "failed")
241 return resp, err
242 }
243
244 if err := l.bootstrapAttachments(ctx, req, addStep); err != nil {
245 return resp, err
246 }
247
248 if err := l.bootstrapSecrets(ctx, req, addStep); err != nil {
249 return resp, err
250 }
251
252 if err := l.bootstrapAttachmentSecrets(ctx, req, addStep); err != nil {
253 return resp, err
254 }
255
256 return resp, nil
257}
258
259func (l *localControlPlane) bootstrapAttachments(ctx context.Context, req dto.BootstrapRequest, addStep func(name, status string)) error {
260 for _, attachment := range req.Attachments {
261 err := l.configSvc.AddAttachment(ctx, req.Domain, attachment)
262 if err == nil {
263 addStep("attachment:"+attachment, "created")
264 continue
265 }
266 if errors.Is(err, domain.ErrAttachmentExists) {
267 addStep("attachment:"+attachment, "noop")
268 continue
269 }
270 addStep("attachment:"+attachment, "failed")
271 return err
272 }
273
274 return nil
275}
276
277func (l *localControlPlane) bootstrapSecrets(ctx context.Context, req dto.BootstrapRequest, addStep func(name, status string)) error {
278 if len(req.Env) == 0 {
279 return nil
280 }
281
282 if err := l.secretSvc.Set(ctx, req.Domain, req.Env); err != nil {
283 addStep("env", "failed")
284 return err
285 }
286
287 addStep("env", "updated")
288 return nil
289}
290
291func (l *localControlPlane) bootstrapAttachmentSecrets(ctx context.Context, req dto.BootstrapRequest, addStep func(name, status string)) error {
292 for service, env := range req.AttachmentEnv {
293 if err := l.secretSvc.SetAttachment(ctx, req.Domain, service, env); err != nil {
294 addStep("attachment_env:"+service, "failed")
295 return err
296 }
297 addStep("attachment_env:"+service, "updated")
298 }
299
300 return nil
301}
302
303func (l *localControlPlane) ListSecretsWithAttachments(ctx context.Context, secretDomain string) (*remote.SecretsListResult, error) {
304 if l.secretSvc == nil {
305 return nil, fmt.Errorf("local secret service unavailable")
306 }
307
308 keys, attachments, err := l.secretSvc.ListKeysWithAttachments(ctx, secretDomain)
309 if err != nil {
310 return nil, fmt.Errorf("list secret keys with attachments: %w", err)
311 }
312
313 result := &remote.SecretsListResult{
314 Domain: secretDomain,
315 Keys: keys,
316 }
317 for _, att := range attachments {
318 result.Attachments = append(result.Attachments, remote.AttachmentSecrets{
319 Service: att.Service,
320 Keys: att.Keys,
321 })
322 }
323
324 return result, nil
325}
326
327func (l *localControlPlane) SetSecrets(ctx context.Context, secretDomain string, secrets map[string]string) error {
328 if l.secretSvc == nil {
329 return fmt.Errorf("local secret service unavailable")
330 }
331 return l.secretSvc.Set(ctx, secretDomain, secrets)
332}
333
334func (l *localControlPlane) DeleteSecret(ctx context.Context, secretDomain, key string) error {
335 if l.secretSvc == nil {
336 return fmt.Errorf("local secret service unavailable")
337 }
338 return l.secretSvc.Delete(ctx, secretDomain, key)
339}
340
341func (l *localControlPlane) SetAttachmentSecrets(ctx context.Context, domainName, service string, secrets map[string]string) error {
342 if l.secretSvc == nil {
343 return fmt.Errorf("local secret service unavailable")
344 }
345 return l.secretSvc.SetAttachment(ctx, domainName, service, secrets)
346}
347
348func (l *localControlPlane) DeleteAttachmentSecret(ctx context.Context, domainName, service, key string) error {
349 if l.secretSvc == nil {
350 return fmt.Errorf("local secret service unavailable")
351 }
352 return l.secretSvc.DeleteAttachment(ctx, domainName, service, key)
353}
354
355func (l *localControlPlane) ListOrphanedAttachments(ctx context.Context) ([]domain.CleanupAttachment, error) {
356 if l.containerSvc == nil {
357 return nil, fmt.Errorf("local container service unavailable")
358 }
359 attachments, err := l.containerSvc.ListOrphanedAttachments(ctx)
360 if err != nil {
361 return nil, fmt.Errorf("list orphaned attachments: %w", err)
362 }
363 return attachments, nil
364}
365
366func (l *localControlPlane) CleanupOrphanedAttachments(ctx context.Context, owner string, stop bool) (*domain.CleanupReport, error) {
367 if l.containerSvc == nil {
368 return nil, fmt.Errorf("local container service unavailable")
369 }
370 report, err := l.containerSvc.CleanupOrphanedAttachments(ctx, owner, stop)
371 if err != nil {
372 return nil, fmt.Errorf("cleanup orphaned attachments: %w", err)
373 }
374 return report, nil
375}
376
377func (l *localControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
378 if l.configSvc == nil {
379 return nil, fmt.Errorf("local config service unavailable")
380 }
381 return l.configSvc.GetAllAttachments(ctx), nil
382}
383
384func (l *localControlPlane) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) {
385 if l.configSvc == nil {
386 return nil, fmt.Errorf("local config service unavailable")
387 }
388 return l.configSvc.GetAttachmentsFor(ctx, domainOrGroup)
389}
390
391func (l *localControlPlane) FindAttachmentTargetsByImage(ctx context.Context, imageName string) ([]string, error) {
392 if l.configSvc == nil {
393 return nil, fmt.Errorf("local config service unavailable")
394 }
395 return l.configSvc.FindAttachmentTargetsByImage(ctx, imageName), nil
396}
397
398func (l *localControlPlane) AddAttachment(ctx context.Context, domainOrGroup, image string) error {
399 if l.configSvc == nil {
400 return fmt.Errorf("local config service unavailable")
401 }
402 return l.configSvc.AddAttachment(ctx, domainOrGroup, image)
403}
404
405func (l *localControlPlane) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error {
406 if l.configSvc == nil {
407 return fmt.Errorf("local config service unavailable")
408 }
409 return l.configSvc.RemoveAttachment(ctx, domainOrGroup, image)
410}
411
412func (l *localControlPlane) GetAutoRouteAllowedDomains(ctx context.Context) ([]string, error) {
413 if l.configSvc == nil {
414 return nil, fmt.Errorf("local config service unavailable")
415 }
416 return l.configSvc.GetAutoRouteAllowedDomains(ctx)
417}
418
419func (l *localControlPlane) AddAutoRouteAllowedDomain(ctx context.Context, pattern string) error {
420 if l.configSvc == nil {
421 return fmt.Errorf("local config service unavailable")
422 }
423 return l.configSvc.AddAutoRouteAllowedDomain(ctx, pattern)
424}
425
426func (l *localControlPlane) RemoveAutoRouteAllowedDomain(ctx context.Context, pattern string) error {
427 if l.configSvc == nil {
428 return fmt.Errorf("local config service unavailable")
429 }
430 return l.configSvc.RemoveAutoRouteAllowedDomain(ctx, pattern)
431}
432
433func (l *localControlPlane) GetTLSStatus(ctx context.Context) (*dto.TLSStatusResponse, error) {
434 if l.publicTLSSvc == nil {
435 return &dto.TLSStatusResponse{
436 ACMEEnabled: false,
437 SelectionReason: "public TLS service not configured",
438 }, nil
439 }
440
441 status := l.publicTLSSvc.Status(ctx)
442 result := dto.TLSStatusFromDomain(status)
443 return &result, nil
444}
445
446func (l *localControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
447 if l.configSvc == nil {
448 return nil, fmt.Errorf("local config service unavailable")
449 }
450
451 status := &remote.Status{
452 Routes: len(l.configSvc.GetRoutes(ctx)),
453 RegistryDomain: l.configSvc.GetRegistryDomain(),
454 RegistryPort: l.configSvc.GetRegistryPort(),
455 ServerPort: l.configSvc.GetServerPort(),
456 AutoRoute: l.configSvc.IsAutoRouteEnabled(),
457 NetworkIsolation: l.configSvc.IsNetworkIsolationEnabled(),
458 ContainerStatus: map[string]string{},
459 }
460
461 if l.containerSvc != nil {
462 for domainName, container := range l.containerSvc.List(ctx) {
463 if container == nil {
464 continue
465 }
466 status.ContainerStatus[domainName] = container.Status
467 }
468 }
469
470 return status, nil
471}
472
473func (l *localControlPlane) Reload(_ context.Context) error {
474 return app.SendReloadSignal()
475}
476
477func (l *localControlPlane) ListNetworks(ctx context.Context) ([]*domain.NetworkInfo, error) {
478 if l.containerSvc == nil {
479 return nil, fmt.Errorf("container service unavailable")
480 }
481 return l.containerSvc.ListNetworks(ctx)
482}
483
484func (l *localControlPlane) GetConfig(ctx context.Context) (*remote.Config, error) {
485 if l.configSvc == nil {
486 return nil, fmt.Errorf("config service unavailable")
487 }
488 externalRoutes := l.configSvc.GetExternalRoutes()
489 externalResponses := make([]remote.ExternalRoute, 0, len(externalRoutes))
490 for domainName := range externalRoutes {
491 externalResponses = append(externalResponses, remote.ExternalRoute{Domain: domainName})
492 }
493 sort.Slice(externalResponses, func(i, j int) bool {
494 return externalResponses[i].Domain < externalResponses[j].Domain
495 })
496 cfg := &remote.Config{
497 Routes: l.configSvc.GetRoutes(ctx),
498 ExternalRoutes: externalResponses,
499 }
500 cfg.Server.Port = l.configSvc.GetServerPort()
501 cfg.Server.RegistryPort = l.configSvc.GetRegistryPort()
502 cfg.Server.RegistryDomain = l.configSvc.GetRegistryDomain()
503 cfg.AutoRoute.Enabled = l.configSvc.IsAutoRouteEnabled()
504 cfg.NetworkIsolation.Enabled = l.configSvc.IsNetworkIsolationEnabled()
505 cfg.NetworkIsolation.Prefix = l.configSvc.GetNetworkPrefix()
506 if volumeCfg, ok := any(l.configSvc).(interface{ GetVolumeConfig() (bool, string, bool) }); ok {
507 cfg.Volumes.AutoCreate, cfg.Volumes.Prefix, cfg.Volumes.Preserve = volumeCfg.GetVolumeConfig()
508 }
509 return cfg, nil
510}
511
512func (l *localControlPlane) DeployIntent(_ context.Context, imageName string) error {
513 if l.deployCoord != nil {
514 l.deployCoord.SuppressDeployEvent(imageName)
515 }
516 return nil
517}
518
519func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
520 if l.containerSvc != nil && l.configSvc != nil {
521 route, err := l.configSvc.GetRoute(ctx, deployDomain)
522 if err != nil {
523 return nil, err
524 }
525 container, err := l.containerSvc.Deploy(domain.WithInternalDeploy(ctx), *route)
526 if err != nil {
527 return nil, err
528 }
529 result := &remote.DeployResult{Status: "deployed", Domain: deployDomain}
530 if container != nil {
531 result.ContainerID = container.ID
532 }
533 return result, nil
534 }
535
536 domainName, err := app.SendDeploySignal(deployDomain)
537 if err != nil {
538 return nil, err
539 }
540 return &remote.DeployResult{Status: "queued", Domain: domainName}, nil
541}
542
543func (l *localControlPlane) Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) {
544 if l.containerSvc == nil {
545 if withAttachments {
546 return nil, fmt.Errorf("local restart with attachments requires active local container service")
547 }
548 domainName, err := app.SendDeploySignal(restartDomain)
549 if err != nil {
550 return nil, err
551 }
552 return &remote.RestartResult{Status: "queued", Domain: domainName}, nil
553 }
554
555 if err := l.containerSvc.SyncContainers(ctx); err != nil {
556 return nil, err
557 }
558 if err := l.containerSvc.Restart(ctx, restartDomain, withAttachments); err != nil {
559 return nil, err
560 }
561
562 return &remote.RestartResult{Status: "restarted", Domain: restartDomain}, nil
563}
564
565func (l *localControlPlane) ListTags(ctx context.Context, repository string) ([]string, error) {
566 if l.registrySvc == nil {
567 return nil, fmt.Errorf("local registry service unavailable")
568 }
569 return l.registrySvc.ListTags(ctx, repository)
570}
571
572func (l *localControlPlane) ListBackups(ctx context.Context, backupDomain string) ([]dto.BackupJob, error) {
573 if l.backupSvc == nil {
574 return nil, fmt.Errorf("local backup service unavailable")
575 }
576 jobs, err := l.backupSvc.ListBackups(ctx, backupDomain)
577 if err != nil {
578 return nil, err
579 }
580 return toDTOBackupJobs(jobs), nil
581}
582
583func (l *localControlPlane) BackupStatus(ctx context.Context) ([]dto.BackupJob, error) {
584 if l.backupSvc == nil {
585 return nil, fmt.Errorf("local backup service unavailable")
586 }
587 jobs, err := l.backupSvc.Status(ctx)
588 if err != nil {
589 return nil, err
590 }
591 return toDTOBackupJobs(jobs), nil
592}
593
594func (l *localControlPlane) RunBackup(ctx context.Context, backupDomain, dbName string) (*dto.BackupRunResponse, error) {
595 if l.backupSvc == nil {
596 return nil, fmt.Errorf("local backup service unavailable")
597 }
598 result, err := l.backupSvc.RunBackup(ctx, backupDomain, dbName)
599 if err != nil {
600 return nil, err
601 }
602 if result == nil {
603 return &dto.BackupRunResponse{Status: "ok"}, nil
604 }
605 job := toDTOBackupJob(result.Job)
606 return &dto.BackupRunResponse{Status: "ok", Backup: &job}, nil
607}
608
609func (l *localControlPlane) DetectDatabases(ctx context.Context, backupDomain string) ([]dto.DatabaseInfo, error) {
610 if l.backupSvc == nil {
611 return nil, fmt.Errorf("local backup service unavailable")
612 }
613 dbs, err := l.backupSvc.DetectDatabases(ctx, backupDomain)
614 if err != nil {
615 return nil, err
616 }
617 out := make([]dto.DatabaseInfo, 0, len(dbs))
618 for _, db := range dbs {
619 out = append(out, dto.DatabaseInfo{
620 Type: string(db.Type),
621 Name: db.Name,
622 Version: db.Version,
623 Host: db.Host,
624 Port: db.Port,
625 ContainerID: db.ContainerID,
626 ImageName: db.ImageName,
627 })
628 }
629 return out, nil
630}
631
632func (l *localControlPlane) GetProcessLogs(ctx context.Context, lines int) ([]string, error) {
633 if l.logSvc == nil {
634 return nil, fmt.Errorf("local log service unavailable")
635 }
636 return l.logSvc.GetProcessLogs(ctx, lines)
637}
638
639func (l *localControlPlane) GetContainerLogs(ctx context.Context, logDomain string, lines int) ([]string, error) {
640 if l.logSvc == nil {
641 return nil, fmt.Errorf("local log service unavailable")
642 }
643 return l.logSvc.GetContainerLogs(ctx, logDomain, lines)
644}
645
646func (l *localControlPlane) StreamProcessLogs(ctx context.Context, lines int) (<-chan string, error) {
647 if l.logSvc == nil {
648 return nil, fmt.Errorf("local log service unavailable")
649 }
650 return l.logSvc.FollowProcessLogs(ctx, lines)
651}
652
653func (l *localControlPlane) StreamContainerLogs(ctx context.Context, logDomain string, lines int) (<-chan string, error) {
654 if l.logSvc == nil {
655 return nil, fmt.Errorf("local log service unavailable")
656 }
657 return l.logSvc.FollowContainerLogs(ctx, logDomain, lines)
658}
659
660func (l *localControlPlane) ListVolumes(ctx context.Context) ([]dto.Volume, error) {
661 if l.volumeSvc == nil {
662 return nil, fmt.Errorf("volume service unavailable")
663 }
664 vols, err := l.volumeSvc.ListVolumes(ctx)
665 if err != nil {
666 return nil, err
667 }
668
669 result := make([]dto.Volume, len(vols))
670 for i, v := range vols {
671 result[i] = dto.Volume{
672 Name: v.Name,
673 Driver: v.Driver,
674 MountPoint: v.MountPoint,
675 Size: v.Size,
676 CreatedAt: v.CreatedAt,
677 InUse: v.InUse,
678 Containers: v.Containers,
679 Labels: v.Labels,
680 }
681 }
682 return result, nil
683}
684
685func (l *localControlPlane) PruneVolumes(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error) {
686 if l.volumeSvc == nil {
687 return nil, fmt.Errorf("volume service unavailable")
688 }
689 report, removed, err := l.volumeSvc.PruneVolumes(ctx, req.DryRun)
690 if err != nil {
691 return nil, err
692 }
693
694 vols := make([]dto.Volume, len(removed))
695 for i, v := range removed {
696 vols[i] = dto.Volume{
697 Name: v.Name,
698 Size: v.Size,
699 }
700 }
701
702 return &dto.VolumePruneResponse{
703 VolumesRemoved: report.VolumesRemoved,
704 SpaceReclaimed: report.SpaceReclaimed,
705 Volumes: vols,
706 }, nil
707}
708
709func mergeConfiguredRemoteRouteInfos(configured []domain.Route, detailed []domain.RouteInfo) []remote.RouteInfo {
710 detailedByDomain := make(map[string]domain.RouteInfo, len(detailed))
711 for _, info := range detailed {
712 detailedByDomain[info.Domain] = info
713 }
714
715 merged := make([]domain.RouteInfo, 0, len(configured))
716 for _, route := range configured {
717 info, ok := detailedByDomain[route.Domain]
718 if !ok {
719 info = domain.RouteInfo{Domain: route.Domain, Image: route.Image}
720 } else if info.Image == "" {
721 info.Image = route.Image
722 }
723 merged = append(merged, info)
724 }
725
726 return toRemoteRouteInfos(merged)
727}
728
729func toRemoteRouteInfos(routes []domain.RouteInfo) []remote.RouteInfo {
730 out := make([]remote.RouteInfo, 0, len(routes))
731 for _, route := range routes {
732 attachments := make([]dto.Attachment, 0, len(route.Attachments))
733 for _, attachment := range route.Attachments {
734 attachments = append(attachments, dto.Attachment{
735 Name: attachment.Name,
736 Image: attachment.Image,
737 ContainerID: attachment.ContainerID,
738 Status: attachment.Status,
739 Network: attachment.Network,
740 })
741 }
742
743 out = append(out, remote.RouteInfo{
744 Domain: route.Domain,
745 Image: route.Image,
746 ContainerID: route.ContainerID,
747 ContainerStatus: route.ContainerStatus,
748 Network: route.Network,
749 Attachments: attachments,
750 })
751 }
752 return out
753}
754
755func toDTOBackupJobs(jobs []domain.BackupJob) []dto.BackupJob {
756 out := make([]dto.BackupJob, 0, len(jobs))
757 for _, job := range jobs {
758 out = append(out, toDTOBackupJob(job))
759 }
760 return out
761}
762
763func toDTOBackupJob(job domain.BackupJob) dto.BackupJob {
764 var startedAt *time.Time
765 if !job.StartedAt.IsZero() {
766 t := job.StartedAt
767 startedAt = &t
768 }
769 var completedAt *time.Time
770 if !job.CompletedAt.IsZero() {
771 t := job.CompletedAt
772 completedAt = &t
773 }
774
775 return dto.BackupJob{
776 ID: job.ID,
777 Domain: job.Domain,
778 DBName: job.DBName,
779 Schedule: string(job.Schedule),
780 Type: string(job.Type),
781 Status: string(job.Status),
782 StartedAt: startedAt,
783 CompletedAt: completedAt,
784 SizeBytes: job.SizeBytes,
785 Error: job.Error,
786 }
787}