detached bf2ccb9

refactor(postboot): extract post-boot setup into dedicated internal package

commit
bf2ccb92275b673c716646b7617df95186ca6fbd
author
bnema · · 2025-10-28 21:15 UTC
committer
bnema · · 2025-10-28 22:32 UTC
parent
43e633b
signature
unsigned
1 file · +151 -0
A internal/postboot/setup.go +151 -0
@@ -0,0 +1,151 @@
1+package postboot
2+
3+import (
4+ "fmt"
5+ "io"
6+ "net/http"
7+ "path/filepath"
8+ "strings"
9+
10+ "github.com/bnema/archup/internal/config"
11+ "github.com/bnema/archup/internal/interfaces"
12+ "github.com/bnema/archup/internal/logger"
13+)
14+
15+// SetupResult holds the result of post-boot setup
16+type SetupResult struct {
17+ ScriptsDownloaded int
18+ ServiceEnabled bool
19+}
20+
21+// Setup handles first-boot service configuration
22+type Setup struct {
23+ fs interfaces.FileSystem
24+ http interfaces.HTTPClient
25+ chrExec interfaces.ChrootExecutor
26+ config *config.Config
27+ logger *logger.Logger
28+}
29+
30+// NewSetup creates a new post-boot setup handler
31+func NewSetup(
32+ fs interfaces.FileSystem,
33+ http interfaces.HTTPClient,
34+ chrExec interfaces.ChrootExecutor,
35+ cfg *config.Config,
36+ log *logger.Logger,
37+) *Setup {
38+ return &Setup{
39+ fs: fs,
40+ http: http,
41+ chrExec: chrExec,
42+ config: cfg,
43+ logger: log,
44+ }
45+}
46+
47+// Configure downloads and configures first-boot service
48+func (s *Setup) Configure() (SetupResult, error) {
49+ result := SetupResult{
50+ ScriptsDownloaded: 0,
51+ ServiceEnabled: false,
52+ }
53+
54+ if err := s.fs.MkdirAll(config.PathMntPostBoot, 0755); err != nil {
55+ return result, fmt.Errorf("failed to create post-boot directory: %w", err)
56+ }
57+
58+ // Download logo.txt
59+ logoURL := fmt.Sprintf("%s/logo.txt", s.config.RawURL)
60+ resp, err := s.http.Get(logoURL)
61+ switch {
62+ case err != nil:
63+ return result, fmt.Errorf("failed to download logo.txt: %w", err)
64+ case resp.StatusCode != http.StatusOK:
65+ resp.Body.Close()
66+ return result, fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
67+ }
68+
69+ logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
70+ logoFile, err := s.fs.Create(logoPath)
71+ if err != nil {
72+ resp.Body.Close()
73+ return result, fmt.Errorf("failed to create logo.txt: %w", err)
74+ }
75+
76+ _, err = io.Copy(logoFile, resp.Body)
77+ logoFile.Close()
78+ resp.Body.Close()
79+ if err != nil {
80+ return result, fmt.Errorf("failed to save logo.txt: %w", err)
81+ }
82+
83+ // Download all post-boot scripts
84+ for _, script := range config.PostBootScripts {
85+
86+ scriptURL := fmt.Sprintf("%s/install/post-boot/%s", s.config.RawURL, script)
87+ resp, err := s.http.Get(scriptURL)
88+ switch {
89+ case err != nil:
90+ return result, fmt.Errorf("failed to download %s: %w", script, err)
91+ case resp.StatusCode != http.StatusOK:
92+ resp.Body.Close()
93+ return result, fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
94+ }
95+
96+ scriptPath := filepath.Join(config.PathMntPostBoot, script)
97+ scriptFile, err := s.fs.Create(scriptPath)
98+ if err != nil {
99+ resp.Body.Close()
100+ return result, fmt.Errorf("failed to create %s: %w", script, err)
101+ }
102+
103+ _, err = io.Copy(scriptFile, resp.Body)
104+ scriptFile.Close()
105+ resp.Body.Close()
106+
107+ if err != nil {
108+ return result, fmt.Errorf("failed to save %s: %w", script, err)
109+ }
110+
111+ if err := s.fs.Chmod(scriptPath, 0755); err != nil {
112+ return result, fmt.Errorf("failed to set permissions on %s: %w", script, err)
113+ }
114+ }
115+
116+
117+ // Download service template
118+ serviceURL := fmt.Sprintf("%s/%s", s.config.RawURL, config.PostBootServiceTemplate)
119+ resp, err = s.http.Get(serviceURL)
120+ switch {
121+ case err != nil:
122+ return result, fmt.Errorf("failed to download service template: %w", err)
123+ case resp.StatusCode != http.StatusOK:
124+ resp.Body.Close()
125+ return result, fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
126+ }
127+
128+ templateBytes, err := io.ReadAll(resp.Body)
129+ resp.Body.Close()
130+ if err != nil {
131+ return result, fmt.Errorf("failed to read service template: %w", err)
132+ }
133+
134+ serviceContent := string(templateBytes)
135+ serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_EMAIL__", s.config.Email)
136+ serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_USERNAME__", s.config.Username)
137+
138+ servicePath := filepath.Join(config.PathMntSystemdSystem, config.PostBootServiceName)
139+ if err := s.fs.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
140+ return result, fmt.Errorf("failed to write service file: %w", err)
141+ }
142+
143+
144+ if err := s.chrExec.ChrootSystemctl(s.logger.LogPath(), config.PathMnt, "enable", config.PostBootServiceName); err != nil {
145+ return result, fmt.Errorf("failed to enable service: %w", err)
146+ }
147+
148+ result.ServiceEnabled = true
149+ result.ScriptsDownloaded = len(config.PostBootScripts)
150+ return result, nil
151+}
@@ -0,0 +1,151 @@
1+package postboot
2+
3+import (
4+ "fmt"
5+ "io"
6+ "net/http"
7+ "path/filepath"
8+ "strings"
9+
10+ "github.com/bnema/archup/internal/config"
11+ "github.com/bnema/archup/internal/interfaces"
12+ "github.com/bnema/archup/internal/logger"
13+)
14+
15+// SetupResult holds the result of post-boot setup
16+type SetupResult struct {
17+ ScriptsDownloaded int
18+ ServiceEnabled bool
19+}
20+
21+// Setup handles first-boot service configuration
22+type Setup struct {
23+ fs interfaces.FileSystem
24+ http interfaces.HTTPClient
25+ chrExec interfaces.ChrootExecutor
26+ config *config.Config
27+ logger *logger.Logger
28+}
29+
30+// NewSetup creates a new post-boot setup handler
31+func NewSetup(
32+ fs interfaces.FileSystem,
33+ http interfaces.HTTPClient,
34+ chrExec interfaces.ChrootExecutor,
35+ cfg *config.Config,
36+ log *logger.Logger,
37+) *Setup {
38+ return &Setup{
39+ fs: fs,
40+ http: http,
41+ chrExec: chrExec,
42+ config: cfg,
43+ logger: log,
44+ }
45+}
46+
47+// Configure downloads and configures first-boot service
48+func (s *Setup) Configure() (SetupResult, error) {
49+ result := SetupResult{
50+ ScriptsDownloaded: 0,
51+ ServiceEnabled: false,
52+ }
53+
54+ if err := s.fs.MkdirAll(config.PathMntPostBoot, 0755); err != nil {
55+ return result, fmt.Errorf("failed to create post-boot directory: %w", err)
56+ }
57+
58+ // Download logo.txt
59+ logoURL := fmt.Sprintf("%s/logo.txt", s.config.RawURL)
60+ resp, err := s.http.Get(logoURL)
61+ switch {
62+ case err != nil:
63+ return result, fmt.Errorf("failed to download logo.txt: %w", err)
64+ case resp.StatusCode != http.StatusOK:
65+ resp.Body.Close()
66+ return result, fmt.Errorf("failed to download logo.txt: HTTP %d", resp.StatusCode)
67+ }
68+
69+ logoPath := filepath.Join(config.PathMntPostBoot, "logo.txt")
70+ logoFile, err := s.fs.Create(logoPath)
71+ if err != nil {
72+ resp.Body.Close()
73+ return result, fmt.Errorf("failed to create logo.txt: %w", err)
74+ }
75+
76+ _, err = io.Copy(logoFile, resp.Body)
77+ logoFile.Close()
78+ resp.Body.Close()
79+ if err != nil {
80+ return result, fmt.Errorf("failed to save logo.txt: %w", err)
81+ }
82+
83+ // Download all post-boot scripts
84+ for _, script := range config.PostBootScripts {
85+
86+ scriptURL := fmt.Sprintf("%s/install/post-boot/%s", s.config.RawURL, script)
87+ resp, err := s.http.Get(scriptURL)
88+ switch {
89+ case err != nil:
90+ return result, fmt.Errorf("failed to download %s: %w", script, err)
91+ case resp.StatusCode != http.StatusOK:
92+ resp.Body.Close()
93+ return result, fmt.Errorf("failed to download %s: HTTP %d", script, resp.StatusCode)
94+ }
95+
96+ scriptPath := filepath.Join(config.PathMntPostBoot, script)
97+ scriptFile, err := s.fs.Create(scriptPath)
98+ if err != nil {
99+ resp.Body.Close()
100+ return result, fmt.Errorf("failed to create %s: %w", script, err)
101+ }
102+
103+ _, err = io.Copy(scriptFile, resp.Body)
104+ scriptFile.Close()
105+ resp.Body.Close()
106+
107+ if err != nil {
108+ return result, fmt.Errorf("failed to save %s: %w", script, err)
109+ }
110+
111+ if err := s.fs.Chmod(scriptPath, 0755); err != nil {
112+ return result, fmt.Errorf("failed to set permissions on %s: %w", script, err)
113+ }
114+ }
115+
116+
117+ // Download service template
118+ serviceURL := fmt.Sprintf("%s/%s", s.config.RawURL, config.PostBootServiceTemplate)
119+ resp, err = s.http.Get(serviceURL)
120+ switch {
121+ case err != nil:
122+ return result, fmt.Errorf("failed to download service template: %w", err)
123+ case resp.StatusCode != http.StatusOK:
124+ resp.Body.Close()
125+ return result, fmt.Errorf("failed to download service template: HTTP %d", resp.StatusCode)
126+ }
127+
128+ templateBytes, err := io.ReadAll(resp.Body)
129+ resp.Body.Close()
130+ if err != nil {
131+ return result, fmt.Errorf("failed to read service template: %w", err)
132+ }
133+
134+ serviceContent := string(templateBytes)
135+ serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_EMAIL__", s.config.Email)
136+ serviceContent = strings.ReplaceAll(serviceContent, "__ARCHUP_USERNAME__", s.config.Username)
137+
138+ servicePath := filepath.Join(config.PathMntSystemdSystem, config.PostBootServiceName)
139+ if err := s.fs.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
140+ return result, fmt.Errorf("failed to write service file: %w", err)
141+ }
142+
143+
144+ if err := s.chrExec.ChrootSystemctl(s.logger.LogPath(), config.PathMnt, "enable", config.PostBootServiceName); err != nil {
145+ return result, fmt.Errorf("failed to enable service: %w", err)
146+ }
147+
148+ result.ServiceEnabled = true
149+ result.ScriptsDownloaded = len(config.PostBootScripts)
150+ return result, nil
151+}

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