main 6af0a61

installation.go RawHistoryFolder

1package installation
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"time"
8
9	"github.com/google/uuid"
10)
11
12// Installation is the aggregate root entity representing the entire installation process
13// It manages the lifecycle of an Arch Linux installation from start to completion
14type Installation struct {
15	// Unique identifier for this installation
16	id string
17
18	// Current state of the installation
19	state State
20
21	// Configuration for the installation (hostname, username, etc.)
22	// These are immutable once set
23	hostname       string
24	username       string
25	targetDisk     string
26	encryptionType string
27
28	// Installation metadata
29	createdAt   time.Time
30	startedAt   *time.Time
31	completedAt *time.Time
32
33	// Domain events that have occurred during this installation
34	events []DomainEvent
35}
36
37// NewInstallation creates a new Installation aggregate
38// All parameters are validated according to business rules
39func NewInstallation(
40	hostname string,
41	username string,
42	targetDisk string,
43	encryptionType string,
44) (*Installation, error) {
45	// Validate hostname
46	if hostname == "" {
47		return nil, errors.New("hostname cannot be empty")
48	}
49	if len(hostname) > 63 {
50		return nil, errors.New("hostname cannot exceed 63 characters")
51	}
52
53	// Validate username
54	if username == "" {
55		return nil, errors.New("username cannot be empty")
56	}
57	if len(username) > 32 {
58		return nil, errors.New("username cannot exceed 32 characters")
59	}
60
61	// Validate targetDisk
62	if targetDisk == "" {
63		return nil, errors.New("target disk cannot be empty")
64	}
65
66	// Validate encryptionType
67	validEncryption := map[string]bool{
68		"none":     true,
69		"luks":     true,
70		"luks-lvm": true,
71	}
72	if !validEncryption[encryptionType] {
73		return nil, fmt.Errorf("invalid encryption type: %s", encryptionType)
74	}
75
76	installation := &Installation{
77		id:             uuid.New().String(),
78		state:          StateNotStarted,
79		hostname:       hostname,
80		username:       username,
81		targetDisk:     targetDisk,
82		encryptionType: encryptionType,
83		createdAt:      time.Now().UTC(),
84		events:         []DomainEvent{},
85	}
86
87	return installation, nil
88}
89
90// ID returns the unique identifier
91func (i *Installation) ID() string {
92	return i.id
93}
94
95// State returns the current installation state
96func (i *Installation) State() State {
97	return i.state
98}
99
100// Hostname returns the configured hostname
101func (i *Installation) Hostname() string {
102	return i.hostname
103}
104
105// Username returns the configured username
106func (i *Installation) Username() string {
107	return i.username
108}
109
110// TargetDisk returns the target installation disk
111func (i *Installation) TargetDisk() string {
112	return i.targetDisk
113}
114
115// EncryptionType returns the encryption type ("none", "luks", or "luks-lvm")
116func (i *Installation) EncryptionType() string {
117	return i.encryptionType
118}
119
120// CreatedAt returns when this installation was created
121func (i *Installation) CreatedAt() time.Time {
122	return i.createdAt
123}
124
125// StartedAt returns when the installation actually started (or nil if not started)
126func (i *Installation) StartedAt() *time.Time {
127	return i.startedAt
128}
129
130// CompletedAt returns when the installation completed (or nil if not completed)
131func (i *Installation) CompletedAt() *time.Time {
132	return i.completedAt
133}
134
135// IsStarted returns true if the installation has been started
136func (i *Installation) IsStarted() bool {
137	return i.state != StateNotStarted
138}
139
140// IsCompleted returns true if the installation has completed (successfully or failed)
141func (i *Installation) IsCompleted() bool {
142	return i.state.IsTerminal()
143}
144
145// IsSuccessful returns true if the installation completed successfully
146func (i *Installation) IsSuccessful() bool {
147	return i.state == StateCompleted
148}
149
150// IsFailed returns true if the installation failed
151func (i *Installation) IsFailed() bool {
152	return i.state == StateFailed
153}
154
155// Start transitions the installation from NotStarted to PreflightChecks
156// This is the entry point that validates prerequisites and begins the process
157func (i *Installation) Start(ctx context.Context) error {
158	if i.state != StateNotStarted {
159		return ErrInstallationAlreadyStarted
160	}
161
162	now := time.Now().UTC()
163	i.startedAt = &now
164
165	// Transition to preflight checks
166	if err := i.state.TransitionTo(StatePreflightChecks); err != nil {
167		return err
168	}
169	i.state = StatePreflightChecks
170
171	// Emit event
172	event := NewInstallationStartedEvent(
173		i.id,
174		i.hostname,
175		i.username,
176		i.targetDisk,
177		i.encryptionType,
178	)
179	i.recordEvent(event)
180
181	return nil
182}
183
184// TransitionToNextPhase moves the installation to the next phase in the sequence
185func (i *Installation) TransitionToNextPhase() error {
186	if i.state.IsTerminal() {
187		return ErrInstallationAlreadyCompleted
188	}
189
190	nextState := i.getNextPhase()
191	if nextState == -1 {
192		return ErrInvalidPhaseTransition
193	}
194
195	if err := i.state.TransitionTo(State(nextState)); err != nil {
196		return err
197	}
198	i.state = State(nextState)
199
200	// Emit event
201	event := NewPhaseStartedEvent(i.id, i.state)
202	i.recordEvent(event)
203
204	return nil
205}
206
207// CompleteCurrentPhase marks the current phase as completed and transitions to next
208func (i *Installation) CompleteCurrentPhase(durationSeconds int) error {
209	if i.state == StateNotStarted || i.state.IsTerminal() {
210		return ErrInstallationNotStarted
211	}
212
213	currentPhase := i.state
214
215	// Record completion event
216	nextPhase := i.getNextPhase()
217	event := NewPhaseCompletedEvent(i.id, currentPhase, State(nextPhase), durationSeconds)
218	i.recordEvent(event)
219
220	// Transition to next phase
221	return i.TransitionToNextPhase()
222}
223
224// FailCurrentPhase marks the current phase as failed
225func (i *Installation) FailCurrentPhase(errorMessage string, recoverable bool) error {
226	if i.state == StateNotStarted {
227		return ErrInstallationNotStarted
228	}
229
230	if i.state.IsTerminal() {
231		return ErrInstallationAlreadyCompleted
232	}
233
234	currentPhase := i.state
235
236	// Record failure event
237	event := NewPhaseFailedEvent(i.id, currentPhase, errorMessage, recoverable)
238	i.recordEvent(event)
239
240	// Transition to failed state
241	if err := i.state.TransitionTo(StateFailed); err != nil {
242		return err
243	}
244	i.state = StateFailed
245
246	// Record installation failure event
247	failureEvent := NewInstallationFailedEvent(i.id, currentPhase, errorMessage)
248	i.recordEvent(failureEvent)
249
250	return nil
251}
252
253// Complete marks the installation as successfully completed
254// Can be called from any non-terminal state (phases are tracked by progress tracker, not state machine)
255func (i *Installation) Complete(totalDurationSeconds int) error {
256	if i.state.IsTerminal() {
257		return ErrInstallationAlreadyCompleted
258	}
259	if i.state == StateNotStarted {
260		return ErrInstallationNotStarted
261	}
262
263	now := time.Now().UTC()
264	i.completedAt = &now
265	i.state = StateCompleted
266
267	// Emit completion event
268	event := NewInstallationCompletedEvent(i.id, totalDurationSeconds)
269	i.recordEvent(event)
270
271	return nil
272}
273
274// ProgressPercentage returns estimated progress as percentage (0-100)
275func (i *Installation) ProgressPercentage() int {
276	return i.state.ProgressPercentage()
277}
278
279// UncommittedEvents returns all domain events that have occurred but not yet persisted
280func (i *Installation) UncommittedEvents() []DomainEvent {
281	return i.events
282}
283
284// ClearEvents clears the uncommitted events after persistence
285func (i *Installation) ClearEvents() {
286	i.events = []DomainEvent{}
287}
288
289// Private methods
290
291// recordEvent adds a domain event to the uncommitted events list
292func (i *Installation) recordEvent(event DomainEvent) {
293	i.events = append(i.events, event)
294}
295
296// getNextPhase returns the next phase in the sequence
297func (i *Installation) getNextPhase() State {
298	sequence := PhaseSequence()
299	currentIndex := PhaseIndex(i.state)
300
301	if currentIndex == -1 || currentIndex >= len(sequence)-1 {
302		return -1
303	}
304
305	return sequence[currentIndex+1]
306}

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