1package cli
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "github.com/bnema/gordon/internal/adapters/dto"
9 "github.com/bnema/gordon/internal/adapters/in/cli/remote"
10 "github.com/bnema/gordon/internal/app"
11 "github.com/bnema/gordon/internal/boundaries/in"
12 "github.com/bnema/gordon/internal/domain"
13)
14
15type localControlPlane struct {
16 configSvc in.ConfigService
17 secretSvc in.SecretService
18 containerSvc in.ContainerService
19 backupSvc in.BackupService
20 registrySvc in.RegistryService
21 healthSvc in.HealthService
22 logSvc in.LogService
23}
24
25func NewLocalControlPlane(kernel *app.Kernel) ControlPlane {
26 if kernel == nil {
27 return &localControlPlane{}
28 }
29
30 return &localControlPlane{
31 configSvc: kernel.Config(),
32 secretSvc: kernel.Secrets(),
33 containerSvc: kernel.Container(),
34 backupSvc: kernel.Backup(),
35 registrySvc: kernel.Registry(),
36 healthSvc: kernel.Health(),
37 logSvc: kernel.Logs(),
38 }
39}
40
41func (l *localControlPlane) ListRoutesWithDetails(ctx context.Context) ([]remote.RouteInfo, error) {
42 if l.containerSvc != nil {
43 return toRemoteRouteInfos(l.containerSvc.ListRoutesWithDetails(ctx)), nil
44 }
45
46 if l.configSvc == nil {
47 return nil, fmt.Errorf("local config service unavailable")
48 }
49
50 routes := l.configSvc.GetRoutes(ctx)
51 infos := make([]remote.RouteInfo, 0, len(routes))
52 for _, route := range routes {
53 infos = append(infos, remote.RouteInfo{Domain: route.Domain, Image: route.Image})
54 }
55 return infos, nil
56}
57
58func (l *localControlPlane) GetHealth(ctx context.Context) (map[string]*remote.RouteHealth, error) {
59 if l.healthSvc == nil {
60 return map[string]*remote.RouteHealth{}, nil
61 }
62
63 health := l.healthSvc.CheckAllRoutes(ctx)
64 result := make(map[string]*remote.RouteHealth, len(health))
65 for domainName, h := range health {
66 if h == nil {
67 continue
68 }
69 result[domainName] = &remote.RouteHealth{
70 ContainerStatus: h.ContainerStatus,
71 HTTPStatus: h.HTTPStatus,
72 ResponseTimeMs: h.ResponseTimeMs,
73 Healthy: h.Healthy,
74 Error: h.Error,
75 }
76 }
77 return result, nil
78}
79
80func (l *localControlPlane) GetRoute(ctx context.Context, routeDomain string) (*domain.Route, error) {
81 if l.configSvc == nil {
82 return nil, fmt.Errorf("local config service unavailable")
83 }
84 return l.configSvc.GetRoute(ctx, routeDomain)
85}
86
87func (l *localControlPlane) FindRoutesByImage(ctx context.Context, imageName string) ([]domain.Route, error) {
88 if l.configSvc == nil {
89 return nil, fmt.Errorf("local config service unavailable")
90 }
91 return l.configSvc.FindRoutesByImage(ctx, imageName), nil
92}
93
94func (l *localControlPlane) AddRoute(ctx context.Context, route domain.Route) error {
95 if l.configSvc == nil {
96 return fmt.Errorf("local config service unavailable")
97 }
98 return l.configSvc.AddRoute(ctx, route)
99}
100
101func (l *localControlPlane) UpdateRoute(ctx context.Context, route domain.Route) error {
102 if l.configSvc == nil {
103 return fmt.Errorf("local config service unavailable")
104 }
105 return l.configSvc.UpdateRoute(ctx, route)
106}
107
108func (l *localControlPlane) RemoveRoute(ctx context.Context, routeDomain string) error {
109 if l.configSvc == nil {
110 return fmt.Errorf("local config service unavailable")
111 }
112 return l.configSvc.RemoveRoute(ctx, routeDomain)
113}
114
115func (l *localControlPlane) ListSecretsWithAttachments(ctx context.Context, secretDomain string) (*remote.SecretsListResult, error) {
116 if l.secretSvc == nil {
117 return nil, fmt.Errorf("local secret service unavailable")
118 }
119
120 keys, attachments, err := l.secretSvc.ListKeysWithAttachments(ctx, secretDomain)
121 if err != nil {
122 return nil, err
123 }
124
125 result := &remote.SecretsListResult{
126 Domain: secretDomain,
127 Keys: keys,
128 }
129 for _, att := range attachments {
130 result.Attachments = append(result.Attachments, remote.AttachmentSecrets{
131 Service: att.Service,
132 Keys: att.Keys,
133 })
134 }
135
136 return result, nil
137}
138
139func (l *localControlPlane) SetSecrets(ctx context.Context, secretDomain string, secrets map[string]string) error {
140 if l.secretSvc == nil {
141 return fmt.Errorf("local secret service unavailable")
142 }
143 return l.secretSvc.Set(ctx, secretDomain, secrets)
144}
145
146func (l *localControlPlane) DeleteSecret(ctx context.Context, secretDomain, key string) error {
147 if l.secretSvc == nil {
148 return fmt.Errorf("local secret service unavailable")
149 }
150 return l.secretSvc.Delete(ctx, secretDomain, key)
151}
152
153func (l *localControlPlane) SetAttachmentSecrets(ctx context.Context, domainName, service string, secrets map[string]string) error {
154 if l.secretSvc == nil {
155 return fmt.Errorf("local secret service unavailable")
156 }
157 return l.secretSvc.SetAttachment(ctx, domainName, service, secrets)
158}
159
160func (l *localControlPlane) DeleteAttachmentSecret(ctx context.Context, domainName, service, key string) error {
161 if l.secretSvc == nil {
162 return fmt.Errorf("local secret service unavailable")
163 }
164 return l.secretSvc.DeleteAttachment(ctx, domainName, service, key)
165}
166
167func (l *localControlPlane) GetAllAttachmentsConfig(ctx context.Context) (map[string][]string, error) {
168 if l.configSvc == nil {
169 return nil, fmt.Errorf("local config service unavailable")
170 }
171 return l.configSvc.GetAllAttachments(ctx), nil
172}
173
174func (l *localControlPlane) GetAttachmentsConfig(ctx context.Context, domainOrGroup string) ([]string, error) {
175 if l.configSvc == nil {
176 return nil, fmt.Errorf("local config service unavailable")
177 }
178 return l.configSvc.GetAttachmentsFor(ctx, domainOrGroup)
179}
180
181func (l *localControlPlane) AddAttachment(ctx context.Context, domainOrGroup, image string) error {
182 if l.configSvc == nil {
183 return fmt.Errorf("local config service unavailable")
184 }
185 return l.configSvc.AddAttachment(ctx, domainOrGroup, image)
186}
187
188func (l *localControlPlane) RemoveAttachment(ctx context.Context, domainOrGroup, image string) error {
189 if l.configSvc == nil {
190 return fmt.Errorf("local config service unavailable")
191 }
192 return l.configSvc.RemoveAttachment(ctx, domainOrGroup, image)
193}
194
195func (l *localControlPlane) GetStatus(ctx context.Context) (*remote.Status, error) {
196 if l.configSvc == nil {
197 return nil, fmt.Errorf("local config service unavailable")
198 }
199
200 status := &remote.Status{
201 Routes: len(l.configSvc.GetRoutes(ctx)),
202 RegistryDomain: l.configSvc.GetRegistryDomain(),
203 RegistryPort: l.configSvc.GetRegistryPort(),
204 ServerPort: l.configSvc.GetServerPort(),
205 AutoRoute: l.configSvc.IsAutoRouteEnabled(),
206 NetworkIsolation: l.configSvc.IsNetworkIsolationEnabled(),
207 ContainerStatus: map[string]string{},
208 }
209
210 if l.containerSvc != nil {
211 for domainName, container := range l.containerSvc.List(ctx) {
212 if container == nil {
213 continue
214 }
215 status.ContainerStatus[domainName] = container.Status
216 }
217 }
218
219 return status, nil
220}
221
222func (l *localControlPlane) Reload(_ context.Context) error {
223 return app.SendReloadSignal()
224}
225
226func (l *localControlPlane) Deploy(ctx context.Context, deployDomain string) (*remote.DeployResult, error) {
227 if l.containerSvc != nil && l.configSvc != nil {
228 route, err := l.configSvc.GetRoute(ctx, deployDomain)
229 if err != nil {
230 return nil, err
231 }
232 container, err := l.containerSvc.Deploy(ctx, *route)
233 if err != nil {
234 return nil, err
235 }
236 result := &remote.DeployResult{Status: "deployed", Domain: deployDomain}
237 if container != nil {
238 result.ContainerID = container.ID
239 }
240 return result, nil
241 }
242
243 domainName, err := app.SendDeploySignal(deployDomain)
244 if err != nil {
245 return nil, err
246 }
247 return &remote.DeployResult{Status: "queued", Domain: domainName}, nil
248}
249
250func (l *localControlPlane) Restart(ctx context.Context, restartDomain string, withAttachments bool) (*remote.RestartResult, error) {
251 if l.containerSvc == nil {
252 if withAttachments {
253 return nil, fmt.Errorf("local restart with attachments requires active local container service")
254 }
255 domainName, err := app.SendDeploySignal(restartDomain)
256 if err != nil {
257 return nil, err
258 }
259 return &remote.RestartResult{Status: "queued", Domain: domainName}, nil
260 }
261
262 if err := l.containerSvc.SyncContainers(ctx); err != nil {
263 return nil, err
264 }
265 if err := l.containerSvc.Restart(ctx, restartDomain, withAttachments); err != nil {
266 return nil, err
267 }
268
269 return &remote.RestartResult{Status: "restarted", Domain: restartDomain}, nil
270}
271
272func (l *localControlPlane) ListTags(ctx context.Context, repository string) ([]string, error) {
273 if l.registrySvc == nil {
274 return nil, fmt.Errorf("local registry service unavailable")
275 }
276 return l.registrySvc.ListTags(ctx, repository)
277}
278
279func (l *localControlPlane) ListBackups(ctx context.Context, backupDomain string) ([]dto.BackupJob, error) {
280 if l.backupSvc == nil {
281 return nil, fmt.Errorf("local backup service unavailable")
282 }
283 jobs, err := l.backupSvc.ListBackups(ctx, backupDomain)
284 if err != nil {
285 return nil, err
286 }
287 return toDTOBackupJobs(jobs), nil
288}
289
290func (l *localControlPlane) BackupStatus(ctx context.Context) ([]dto.BackupJob, error) {
291 if l.backupSvc == nil {
292 return nil, fmt.Errorf("local backup service unavailable")
293 }
294 jobs, err := l.backupSvc.Status(ctx)
295 if err != nil {
296 return nil, err
297 }
298 return toDTOBackupJobs(jobs), nil
299}
300
301func (l *localControlPlane) RunBackup(ctx context.Context, backupDomain, dbName string) (*dto.BackupRunResponse, error) {
302 if l.backupSvc == nil {
303 return nil, fmt.Errorf("local backup service unavailable")
304 }
305 result, err := l.backupSvc.RunBackup(ctx, backupDomain, dbName)
306 if err != nil {
307 return nil, err
308 }
309 if result == nil {
310 return &dto.BackupRunResponse{Status: "ok"}, nil
311 }
312 job := toDTOBackupJob(result.Job)
313 return &dto.BackupRunResponse{Status: "ok", Backup: &job}, nil
314}
315
316func (l *localControlPlane) DetectDatabases(ctx context.Context, backupDomain string) ([]dto.DatabaseInfo, error) {
317 if l.backupSvc == nil {
318 return nil, fmt.Errorf("local backup service unavailable")
319 }
320 dbs, err := l.backupSvc.DetectDatabases(ctx, backupDomain)
321 if err != nil {
322 return nil, err
323 }
324 out := make([]dto.DatabaseInfo, 0, len(dbs))
325 for _, db := range dbs {
326 out = append(out, dto.DatabaseInfo{
327 Type: string(db.Type),
328 Name: db.Name,
329 Version: db.Version,
330 Host: db.Host,
331 Port: db.Port,
332 ContainerID: db.ContainerID,
333 ImageName: db.ImageName,
334 })
335 }
336 return out, nil
337}
338
339func (l *localControlPlane) GetProcessLogs(ctx context.Context, lines int) ([]string, error) {
340 if l.logSvc == nil {
341 return nil, fmt.Errorf("local log service unavailable")
342 }
343 return l.logSvc.GetProcessLogs(ctx, lines)
344}
345
346func (l *localControlPlane) GetContainerLogs(ctx context.Context, logDomain string, lines int) ([]string, error) {
347 if l.logSvc == nil {
348 return nil, fmt.Errorf("local log service unavailable")
349 }
350 return l.logSvc.GetContainerLogs(ctx, logDomain, lines)
351}
352
353func (l *localControlPlane) StreamProcessLogs(ctx context.Context, lines int) (<-chan string, error) {
354 if l.logSvc == nil {
355 return nil, fmt.Errorf("local log service unavailable")
356 }
357 return l.logSvc.FollowProcessLogs(ctx, lines)
358}
359
360func (l *localControlPlane) StreamContainerLogs(ctx context.Context, logDomain string, lines int) (<-chan string, error) {
361 if l.logSvc == nil {
362 return nil, fmt.Errorf("local log service unavailable")
363 }
364 return l.logSvc.FollowContainerLogs(ctx, logDomain, lines)
365}
366
367func toRemoteRouteInfos(routes []domain.RouteInfo) []remote.RouteInfo {
368 out := make([]remote.RouteInfo, 0, len(routes))
369 for _, route := range routes {
370 attachments := make([]dto.Attachment, 0, len(route.Attachments))
371 for _, attachment := range route.Attachments {
372 attachments = append(attachments, dto.Attachment{
373 Name: attachment.Name,
374 Image: attachment.Image,
375 ContainerID: attachment.ContainerID,
376 Status: attachment.Status,
377 Network: attachment.Network,
378 })
379 }
380
381 out = append(out, remote.RouteInfo{
382 Domain: route.Domain,
383 Image: route.Image,
384 ContainerID: route.ContainerID,
385 ContainerStatus: route.ContainerStatus,
386 Network: route.Network,
387 Attachments: attachments,
388 })
389 }
390 return out
391}
392
393func toDTOBackupJobs(jobs []domain.BackupJob) []dto.BackupJob {
394 out := make([]dto.BackupJob, 0, len(jobs))
395 for _, job := range jobs {
396 out = append(out, toDTOBackupJob(job))
397 }
398 return out
399}
400
401func toDTOBackupJob(job domain.BackupJob) dto.BackupJob {
402 var startedAt *time.Time
403 if !job.StartedAt.IsZero() {
404 t := job.StartedAt
405 startedAt = &t
406 }
407 var completedAt *time.Time
408 if !job.CompletedAt.IsZero() {
409 t := job.CompletedAt
410 completedAt = &t
411 }
412
413 return dto.BackupJob{
414 ID: job.ID,
415 Domain: job.Domain,
416 DBName: job.DBName,
417 Schedule: string(job.Schedule),
418 Type: string(job.Type),
419 Status: string(job.Status),
420 StartedAt: startedAt,
421 CompletedAt: completedAt,
422 SizeBytes: job.SizeBytes,
423 Error: job.Error,
424 }
425}