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}