1package app
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "time"
8
9 "github.com/bnema/zerowrap"
10
11 "github.com/bnema/gordon/internal/boundaries/in"
12 configusecase "github.com/bnema/gordon/internal/usecase/config"
13 secretsusecase "github.com/bnema/gordon/internal/usecase/secrets"
14)
15
16// Kernel provides in-process service access for local CLI execution.
17//
18// It intentionally does not start HTTP servers or register signal handlers.
19type Kernel struct {
20 authEnabled bool
21 configSvc in.ConfigService
22 secretSvc in.SecretService
23 containerSvc in.ContainerService
24 backupSvc in.BackupService
25 volumeBackupSvc in.VolumeBackupService
26 registrySvc in.RegistryService
27 healthSvc in.HealthService
28 logSvc in.LogService
29 volumeSvc in.VolumeService
30 publicTLSSvc in.PublicTLSService
31 appSvc in.AppService
32 // appAdmin is the daemon-owned app lifecycle (when the full wiring is
33 // available). Close cancels and joins its in-flight background deploy
34 // executions before any other kernel resource is torn down.
35 appAdmin appAdministration
36 log zerowrap.Logger
37 cleanup func()
38}
39
40// NewKernel initializes local services without starting server listeners.
41func NewKernel(configPath string) (*Kernel, error) {
42 return newKernel(configPath, initLogger)
43}
44
45// NewKernelQuiet initializes local services without emitting console logs.
46func NewKernelQuiet(configPath string) (*Kernel, error) {
47 return newKernel(configPath, quietInitLogger)
48}
49
50type kernelLoggerInit func(cfg Config) (zerowrap.Logger, func(), error)
51
52func newKernel(configPath string, initLog kernelLoggerInit) (*Kernel, error) {
53 ctx := context.Background()
54 v, cfg, err := initConfig(configPath)
55 if err != nil {
56 return nil, err
57 }
58
59 log, cleanup, err := initLog(cfg)
60 if err != nil {
61 return nil, err
62 }
63 if cleanup == nil {
64 cleanup = func() {}
65 }
66
67 ctx = zerowrap.WithCtx(ctx, log)
68
69 // Prefer full service wiring so local CLI can execute the same operations
70 // as remote mode without going through HTTP admin endpoints.
71 // ACME Reconcile and renewal loop are only started from runServers,
72 // so there are no side effects for read-only CLI commands.
73 if svc, fullErr := createServicesWithOptions(ctx, v, cfg, log); fullErr == nil {
74 // Wrap cleanup to stop public TLS service (with its renewal loop)
75 // before the logger is cleaned up.
76 wrappedCleanup := func() {
77 if svc.publicTLSSvc != nil {
78 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
79 defer cancel()
80 if err := svc.publicTLSSvc.Stop(ctx); err != nil {
81 log.Warn().Err(err).Msg("failed to stop public TLS service")
82 }
83 }
84 cleanup()
85 }
86
87 kernel := &Kernel{
88 authEnabled: cfg.Auth.Enabled,
89 configSvc: svc.configSvc,
90 secretSvc: svc.secretSvc,
91 containerSvc: svc.containerSvc,
92 backupSvc: svc.backupSvc,
93 volumeBackupSvc: svc.volumeBackupSvc,
94 registrySvc: svc.registrySvc,
95 healthSvc: svc.healthSvc,
96 logSvc: svc.logSvc,
97 volumeSvc: svc.volumeSvc,
98 publicTLSSvc: svc.publicTLSSvc,
99 appSvc: svc.appSvc,
100 log: log,
101 cleanup: wrappedCleanup,
102 }
103 // A nil *AppServiceImpl must not be stored in the interface: the
104 // interface would be non-nil and Close would call it.
105 if svc.appSvcImpl != nil {
106 kernel.appAdmin = svc.appSvcImpl
107 }
108 return kernel, nil
109 } else {
110 log.Warn().Err(fullErr).Msg("local kernel running in minimal mode")
111 }
112
113 configSvc := configusecase.NewService(v, nil)
114 if err := configSvc.Load(ctx); err != nil {
115 cleanup()
116 return nil, fmt.Errorf("failed to load configuration: %w", err)
117 }
118
119 _, _, _, domainSecretStore, err := createDomainSecretStore(cfg, log)
120 if err != nil {
121 cleanup()
122 return nil, fmt.Errorf("failed to create local secret store: %w", err)
123 }
124
125 secretSvc := secretsusecase.NewService(domainSecretStore, log, nil)
126
127 return &Kernel{
128 authEnabled: cfg.Auth.Enabled,
129 configSvc: configSvc,
130 secretSvc: secretSvc,
131 log: log,
132 cleanup: cleanup,
133 }, nil
134}
135
136func quietInitLogger(Config) (zerowrap.Logger, func(), error) {
137 return zerowrap.New(zerowrap.Config{Level: "disabled", Output: io.Discard}), func() {}, nil
138}
139
140// Close tears the kernel down. It first cancels and joins daemon-owned app
141// administration on a bounded context, so a background deploy execution is
142// never torn down mid-flight alongside the state and runtime it uses. When
143// that quiescence times out the remaining cleanup is skipped and the error is
144// returned: an unfinished execution may still be writing to state.
145func (k *Kernel) Close() error {
146 if k == nil {
147 return nil
148 }
149 if k.appAdmin != nil {
150 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
151 err := quiesceAppAdministration(ctx, k.appAdmin, k.log)
152 cancel()
153 if err != nil {
154 return err
155 }
156 }
157 if k.cleanup != nil {
158 k.cleanup()
159 }
160 return nil
161}
162
163func (k *Kernel) Config() in.ConfigService { return k.configSvc }
164
165func (k *Kernel) Secrets() in.SecretService { return k.secretSvc }
166
167func (k *Kernel) Container() in.ContainerService { return k.containerSvc }
168
169func (k *Kernel) Backup() in.BackupService { return k.backupSvc }
170
171func (k *Kernel) VolumeBackup() in.VolumeBackupService { return k.volumeBackupSvc }
172
173func (k *Kernel) Registry() in.RegistryService { return k.registrySvc }
174
175func (k *Kernel) Health() in.HealthService { return k.healthSvc }
176
177func (k *Kernel) Logs() in.LogService { return k.logSvc }
178
179func (k *Kernel) Volumes() in.VolumeService { return k.volumeSvc }
180
181func (k *Kernel) PublicTLS() in.PublicTLSService { return k.publicTLSSvc }
182
183func (k *Kernel) Apps() in.AppService { return k.appSvc }
184
185func (k *Kernel) AuthEnabled() bool { return k != nil && k.authEnabled }