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