Skip to content

Commit 73e8877

Browse files
authored
fix: preflight should check binary exists and directory executable (#170)
1 parent 8d64840 commit 73e8877

File tree

2 files changed

+68
-19
lines changed

2 files changed

+68
-19
lines changed

app/services/updatepreflight/preflight.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package updatepreflight
33
import (
44
"fmt"
55
"os"
6+
"path/filepath"
7+
8+
"golang.org/x/sys/unix"
69
)
710

811
const (
@@ -46,7 +49,7 @@ func New(cfg PreflightConfig) *PreflightChecker {
4649
func (p *PreflightChecker) Check(requiredSpace int64) *PreflightResult {
4750
var errs []string
4851

49-
if err := p.checkBinaryWritable(); err != nil {
52+
if err := p.checkInstallPath(); err != nil {
5053
errs = append(errs, err.Error())
5154
}
5255

@@ -64,12 +67,21 @@ func (p *PreflightChecker) Check(requiredSpace int64) *PreflightResult {
6467
}
6568
}
6669

67-
func (p *PreflightChecker) checkBinaryWritable() error {
68-
f, err := os.OpenFile(p.agentBinaryPath, os.O_WRONLY, 0)
69-
if err != nil {
70-
return fmt.Errorf("agent binary %s is not writable: %w", p.agentBinaryPath, err)
70+
func (p *PreflightChecker) checkInstallPath() error {
71+
// Check that the binary exists
72+
if _, err := os.Stat(p.agentBinaryPath); err != nil {
73+
if os.IsNotExist(err) {
74+
return fmt.Errorf("agent binary does not exist at %s", p.agentBinaryPath)
75+
}
76+
return fmt.Errorf("cannot stat agent binary %s: %w", p.agentBinaryPath, err)
7177
}
72-
f.Close()
78+
79+
// Check that the directory is writable (needed for atomic rename during upgrade)
80+
dir := filepath.Dir(p.agentBinaryPath)
81+
if err := unix.Access(dir, unix.W_OK); err != nil {
82+
return fmt.Errorf("cannot write to install directory %s: %w", dir, err)
83+
}
84+
7385
return nil
7486
}
7587

app/services/updatepreflight/preflight_test.go

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ import (
77
"testing"
88
)
99

10-
func TestCheck_BinaryWritable(t *testing.T) {
10+
func TestCheck_InstallDirNotWritable(t *testing.T) {
1111
dir := t.TempDir()
12-
binaryPath := filepath.Join(dir, "hostlink")
13-
os.WriteFile(binaryPath, []byte("binary"), 0444)
12+
// Create a read-only directory for the binary
13+
readOnlyBinDir := filepath.Join(dir, "bin")
14+
os.MkdirAll(readOnlyBinDir, 0755)
15+
binaryPath := filepath.Join(readOnlyBinDir, "hostlink")
16+
os.WriteFile(binaryPath, []byte("binary"), 0755)
17+
// Make the directory read-only AFTER creating the file
18+
os.Chmod(readOnlyBinDir, 0555)
19+
t.Cleanup(func() { os.Chmod(readOnlyBinDir, 0755) }) // restore for cleanup
1420

1521
checker := New(PreflightConfig{
1622
AgentBinaryPath: binaryPath,
@@ -20,12 +26,12 @@ func TestCheck_BinaryWritable(t *testing.T) {
2026

2127
result := checker.Check(10 * 1024 * 1024) // 10MB required
2228
if result.Passed {
23-
t.Error("expected Passed to be false when binary is not writable")
29+
t.Error("expected Passed to be false when install directory is not writable")
2430
}
25-
assertContainsError(t, result.Errors, "not writable")
31+
assertContainsError(t, result.Errors, "cannot write to install directory")
2632
}
2733

28-
func TestCheck_BinaryWritable_Passes(t *testing.T) {
34+
func TestCheck_InstallPath_Passes(t *testing.T) {
2935
dir := t.TempDir()
3036
binaryPath := filepath.Join(dir, "hostlink")
3137
os.WriteFile(binaryPath, []byte("binary"), 0755)
@@ -101,23 +107,31 @@ func TestCheck_DiskSpaceSufficient(t *testing.T) {
101107

102108
func TestCheck_AllErrorsReported(t *testing.T) {
103109
dir := t.TempDir()
104-
binaryPath := filepath.Join(dir, "hostlink")
105-
os.WriteFile(binaryPath, []byte("binary"), 0444) // not writable
106110

107-
readOnlyDir := filepath.Join(dir, "updates")
108-
os.MkdirAll(readOnlyDir, 0555) // not writable
111+
// Create binary in a read-only directory
112+
readOnlyBinDir := filepath.Join(dir, "bin")
113+
os.MkdirAll(readOnlyBinDir, 0755)
114+
binaryPath := filepath.Join(readOnlyBinDir, "hostlink")
115+
os.WriteFile(binaryPath, []byte("binary"), 0755)
116+
os.Chmod(readOnlyBinDir, 0555) // make read-only after creating file
117+
t.Cleanup(func() { os.Chmod(readOnlyBinDir, 0755) })
118+
119+
// Create read-only updates directory
120+
readOnlyUpdatesDir := filepath.Join(dir, "updates")
121+
os.MkdirAll(readOnlyUpdatesDir, 0555)
122+
t.Cleanup(func() { os.Chmod(readOnlyUpdatesDir, 0755) })
109123

110124
checker := New(PreflightConfig{
111125
AgentBinaryPath: binaryPath,
112-
UpdatesDir: readOnlyDir,
126+
UpdatesDir: readOnlyUpdatesDir,
113127
StatFunc: func(path string) (uint64, error) { return 1024, nil }, // tiny
114128
})
115129

116130
result := checker.Check(10 * 1024 * 1024)
117131
if result.Passed {
118132
t.Error("expected Passed to be false")
119133
}
120-
// Should have 3 errors: binary not writable, dir not writable, disk space
134+
// Should have 3 errors: install dir not writable, updates dir not writable, disk space
121135
if len(result.Errors) < 3 {
122136
t.Errorf("expected at least 3 errors, got %d: %v", len(result.Errors), result.Errors)
123137
}
@@ -155,7 +169,30 @@ func TestCheck_BinaryNotExists(t *testing.T) {
155169
if result.Passed {
156170
t.Error("expected Passed to be false when binary does not exist")
157171
}
158-
assertContainsError(t, result.Errors, "not writable")
172+
assertContainsError(t, result.Errors, "does not exist")
173+
}
174+
175+
func TestCheck_BinaryExistsButDirNotWritable(t *testing.T) {
176+
dir := t.TempDir()
177+
// Create binary in a directory, then make the directory read-only
178+
binDir := filepath.Join(dir, "usr", "bin")
179+
os.MkdirAll(binDir, 0755)
180+
binaryPath := filepath.Join(binDir, "hostlink")
181+
os.WriteFile(binaryPath, []byte("binary"), 0755)
182+
os.Chmod(binDir, 0555) // read-only after file creation
183+
t.Cleanup(func() { os.Chmod(binDir, 0755) })
184+
185+
checker := New(PreflightConfig{
186+
AgentBinaryPath: binaryPath,
187+
UpdatesDir: dir,
188+
StatFunc: func(path string) (uint64, error) { return 1 << 30, nil },
189+
})
190+
191+
result := checker.Check(10 * 1024 * 1024)
192+
if result.Passed {
193+
t.Error("expected Passed to be false when binary directory is not writable")
194+
}
195+
assertContainsError(t, result.Errors, "cannot write to install directory")
159196
}
160197

161198
// assertContainsError checks that at least one error string contains the substring.

0 commit comments

Comments
 (0)