main 6af0a61

installation_service.go RawHistoryFolder

1package services
2
3import (
4	"context"
5	"errors"
6	"time"
7
8	"github.com/bnema/archup/internal/application/commands"
9	"github.com/bnema/archup/internal/application/dto"
10	"github.com/bnema/archup/internal/application/handlers"
11	"github.com/bnema/archup/internal/domain/installation"
12	"github.com/bnema/archup/internal/domain/ports"
13)
14
15// InstallationService orchestrates the entire installation process
16type InstallationService struct {
17	// Domain
18	installAgg *installation.Installation
19
20	// Handlers
21	bootstrapHandler   *handlers.BootstrapHandler
22	preflightHandler   *handlers.PreflightHandler
23	partitionHandler   *handlers.PartitionHandler
24	baseHandler        *handlers.InstallBaseHandler
25	configHandler      *handlers.ConfigureSystemHandler
26	bootloaderHandler  *handlers.BootloaderHandler
27	reposHandler       *handlers.ReposHandler
28	postInstallHandler *handlers.PostInstallHandler
29
30	// Ports
31	repo   ports.InstallationRepository
32	logger ports.Logger
33
34	// Progress tracking
35	tracker *ProgressTracker
36
37	// State
38	startTime time.Time
39}
40
41// NewInstallationService creates a new installation service with all handlers
42func NewInstallationService(
43	repo ports.InstallationRepository,
44	logger ports.Logger,
45	bootstrapHandler *handlers.BootstrapHandler,
46	preflightHandler *handlers.PreflightHandler,
47	partitionHandler *handlers.PartitionHandler,
48	baseHandler *handlers.InstallBaseHandler,
49	configHandler *handlers.ConfigureSystemHandler,
50	bootloaderHandler *handlers.BootloaderHandler,
51	reposHandler *handlers.ReposHandler,
52	postInstallHandler *handlers.PostInstallHandler,
53) *InstallationService {
54	return &InstallationService{
55		repo:               repo,
56		logger:             logger,
57		bootstrapHandler:   bootstrapHandler,
58		preflightHandler:   preflightHandler,
59		partitionHandler:   partitionHandler,
60		baseHandler:        baseHandler,
61		configHandler:      configHandler,
62		bootloaderHandler:  bootloaderHandler,
63		reposHandler:       reposHandler,
64		postInstallHandler: postInstallHandler,
65		tracker:            NewProgressTracker(),
66	}
67}
68
69// Start begins a new installation
70func (s *InstallationService) Start(ctx context.Context, hostname string, username string, targetDisk string, encryptionType string) error {
71	s.logger.Info("Initializing installation", "hostname", hostname, "username", username)
72
73	// Create installation aggregate
74	inst, err := installation.NewInstallation(hostname, username, targetDisk, encryptionType)
75	if err != nil {
76		s.logger.Error("Failed to create installation", "error", err)
77		s.tracker.EmitPhaseError("initialization", 0, 8, err.Error())
78		return err
79	}
80
81	s.installAgg = inst
82	s.startTime = time.Now()
83
84	// Start the installation
85	if err := s.installAgg.Start(ctx); err != nil {
86		s.logger.Error("Failed to start installation", "error", err)
87		s.tracker.EmitPhaseError("initialization", 0, 8, err.Error())
88		return err
89	}
90
91	return nil
92}
93
94// RunPreflight runs preflight checks
95func (s *InstallationService) RunPreflight(ctx context.Context) (*dto.PreflightResult, error) {
96	if s.installAgg == nil {
97		return nil, errors.New("installation not started")
98	}
99
100	s.tracker.EmitPhaseStarted("Preflight Checks", 1, 8)
101
102	result, err := s.preflightHandler.Handle(ctx, commands.PreflightCommand{})
103	if err != nil {
104		s.tracker.EmitPhaseError("Preflight Checks", 1, 8, err.Error())
105		return nil, err
106	}
107
108	if !result.ChecksPassed {
109		errMsg := "Preflight checks failed"
110		if len(result.CriticalErrors) > 0 {
111			errMsg = result.CriticalErrors[0]
112		}
113		s.tracker.EmitPhaseError("Preflight Checks", 1, 8, errMsg)
114		return result, errors.New(errMsg)
115	}
116
117	s.tracker.EmitPhaseCompleted("Preflight Checks", 1, 8)
118	return result, nil
119}
120
121// RunBootstrap downloads/clones install files
122func (s *InstallationService) RunBootstrap(ctx context.Context) (*dto.BootstrapResult, error) {
123	s.logger.Info("Running bootstrap to prepare install files")
124
125	result, err := s.bootstrapHandler.Handle(ctx)
126	if err != nil {
127		s.logger.Error("Bootstrap failed", "error", err)
128		return result, err
129	}
130
131	s.logger.Info("Bootstrap completed", "method", result.Method)
132	return result, nil
133}
134
135// RunPartition runs disk partitioning
136func (s *InstallationService) RunPartition(ctx context.Context, cmd commands.PartitionDiskCommand) (*dto.PartitionResult, error) {
137	if s.installAgg == nil {
138		return nil, errors.New("installation not started")
139	}
140
141	s.tracker.EmitPhaseStarted("Disk Partitioning", 2, 8)
142
143	result, err := s.partitionHandler.Handle(ctx, cmd)
144	if err != nil {
145		s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, err.Error())
146		return nil, err
147	}
148
149	if !result.Success {
150		s.tracker.EmitPhaseError("Disk Partitioning", 2, 8, result.ErrorDetail)
151		return result, errors.New(result.ErrorDetail)
152	}
153
154	s.tracker.EmitPhaseCompleted("Disk Partitioning", 2, 8)
155	return result, nil
156}
157
158// RunBaseInstall runs base system installation
159func (s *InstallationService) RunBaseInstall(ctx context.Context, cmd commands.InstallBaseCommand) (*dto.InstallBaseResult, error) {
160	if s.installAgg == nil {
161		return nil, errors.New("installation not started")
162	}
163
164	s.tracker.EmitPhaseStarted("Base Installation", 3, 8)
165
166	result, err := s.baseHandler.Handle(ctx, cmd)
167	if err != nil {
168		s.tracker.EmitPhaseError("Base Installation", 3, 8, err.Error())
169		return nil, err
170	}
171
172	if !result.Success {
173		s.tracker.EmitPhaseError("Base Installation", 3, 8, result.ErrorDetail)
174		return result, errors.New(result.ErrorDetail)
175	}
176
177	s.tracker.EmitPhaseCompleted("Base Installation", 3, 8)
178	return result, nil
179}
180
181// RunConfigSystem runs system configuration
182func (s *InstallationService) RunConfigSystem(ctx context.Context, cmd commands.ConfigureSystemCommand) (*dto.ConfigureSystemResult, error) {
183	if s.installAgg == nil {
184		return nil, errors.New("installation not started")
185	}
186
187	s.tracker.EmitPhaseStarted("System Configuration", 4, 8)
188
189	result, err := s.configHandler.Handle(ctx, cmd)
190	if err != nil {
191		s.tracker.EmitPhaseError("System Configuration", 4, 8, err.Error())
192		return nil, err
193	}
194
195	if !result.Success {
196		s.tracker.EmitPhaseError("System Configuration", 4, 8, result.ErrorDetail)
197		return result, errors.New(result.ErrorDetail)
198	}
199
200	s.tracker.EmitPhaseCompleted("System Configuration", 4, 8)
201	return result, nil
202}
203
204// RunBootloaderSetup runs bootloader setup
205func (s *InstallationService) RunBootloaderSetup(ctx context.Context, cmd commands.InstallBootloaderCommand) (*dto.BootloaderResult, error) {
206	if s.installAgg == nil {
207		return nil, errors.New("installation not started")
208	}
209
210	s.tracker.EmitPhaseStarted("Bootloader Setup", 5, 8)
211
212	result, err := s.bootloaderHandler.Handle(ctx, cmd)
213	if err != nil {
214		s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, err.Error())
215		return nil, err
216	}
217
218	if !result.Success {
219		s.tracker.EmitPhaseError("Bootloader Setup", 5, 8, result.ErrorDetail)
220		return result, errors.New(result.ErrorDetail)
221	}
222
223	s.tracker.EmitPhaseCompleted("Bootloader Setup", 5, 8)
224	return result, nil
225}
226
227// RunRepositorySetup runs repository setup
228func (s *InstallationService) RunRepositorySetup(ctx context.Context, cmd commands.SetupRepositoriesCommand) (*dto.RepositoriesResult, error) {
229	if s.installAgg == nil {
230		return nil, errors.New("installation not started")
231	}
232
233	s.tracker.EmitPhaseStarted("Repository Setup", 6, 8)
234
235	result, err := s.reposHandler.Handle(ctx, cmd)
236	if err != nil {
237		s.tracker.EmitPhaseError("Repository Setup", 6, 8, err.Error())
238		return nil, err
239	}
240
241	if !result.Success {
242		s.tracker.EmitPhaseError("Repository Setup", 6, 8, result.ErrorDetail)
243		return result, errors.New(result.ErrorDetail)
244	}
245
246	s.tracker.EmitPhaseCompleted("Repository Setup", 6, 8)
247	return result, nil
248}
249
250// RunPostInstall runs post-installation tasks
251func (s *InstallationService) RunPostInstall(ctx context.Context, cmd commands.PostInstallCommand) (*dto.PostInstallResult, error) {
252	if s.installAgg == nil {
253		return nil, errors.New("installation not started")
254	}
255
256	s.tracker.EmitPhaseStarted("Post-Installation", 7, 8)
257
258	result, err := s.postInstallHandler.Handle(ctx, cmd)
259	if err != nil {
260		s.tracker.EmitPhaseError("Post-Installation", 7, 8, err.Error())
261		return nil, err
262	}
263
264	if !result.Success {
265		s.tracker.EmitPhaseError("Post-Installation", 7, 8, result.ErrorDetail)
266		return result, errors.New(result.ErrorDetail)
267	}
268
269	s.tracker.EmitPhaseCompleted("Post-Installation", 7, 8)
270	return result, nil
271}
272
273// Complete marks installation as complete
274func (s *InstallationService) Complete(ctx context.Context) error {
275	if s.installAgg == nil {
276		return errors.New("installation not started")
277	}
278
279	duration := int(time.Since(s.startTime).Seconds())
280	if err := s.installAgg.Complete(duration); err != nil {
281		s.logger.Error("Failed to complete installation", "error", err)
282		s.tracker.EmitPhaseError("Completion", 8, 8, err.Error())
283		return err
284	}
285
286	s.tracker.EmitPhaseCompleted("Completion", 8, 8)
287	s.logger.Info("Installation completed successfully", "duration", duration)
288
289	return nil
290}
291
292// GetStatus returns the current installation status
293func (s *InstallationService) GetStatus() *dto.InstallationStatus {
294	if s.installAgg == nil {
295		return &dto.InstallationStatus{
296			State: "NotStarted",
297		}
298	}
299
300	completedAt := s.installAgg.CompletedAt()
301	startedAt := s.installAgg.StartedAt()
302
303	return &dto.InstallationStatus{
304		ID:             s.installAgg.ID(),
305		State:          s.installAgg.State().String(),
306		Hostname:       s.installAgg.Hostname(),
307		Username:       s.installAgg.Username(),
308		TargetDisk:     s.installAgg.TargetDisk(),
309		EncryptionType: s.installAgg.EncryptionType(),
310		Progress:       s.installAgg.ProgressPercentage(),
311		StartedAt:      startedAt,
312		CompletedAt:    completedAt,
313		CurrentPhase:   s.installAgg.State().String(),
314	}
315}
316
317// SubscribeProgress returns a channel for progress updates
318func (s *InstallationService) SubscribeProgress() <-chan *dto.ProgressUpdate {
319	return s.tracker.Subscribe()
320}
321
322// Tracker returns the progress tracker (for TUI initialization)
323func (s *InstallationService) Tracker() *ProgressTracker {
324	return s.tracker
325}
326
327// Close closes the service
328func (s *InstallationService) Close() error {
329	s.tracker.Close()
330	return nil
331}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close