Skip to content

Commit 8adaa67

Browse files
authored
fix: unnecessary password prompts for hosts and cert (#19)
1 parent 099b2a8 commit 8adaa67

File tree

6 files changed

+61
-35
lines changed

6 files changed

+61
-35
lines changed

cmd/devbox/up.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,32 +184,29 @@ func runCertUpdate(p *project.Project, firstTime bool) error {
184184
}
185185

186186
func runHostsUpdate(p *project.Project, firstTime, cleanup bool) error {
187-
if len(p.HostEntities) == 0 && !p.HasHosts {
188-
return nil // project has no hosts and there were no hosts before
189-
}
190-
191187
entities := p.HostEntities
192188
if cleanup {
193189
entities = []string{}
194190
}
195191

196-
fmt.Println("[*] Update hosts file...")
197-
198-
err := hosts.Save(p.Name, entities)
192+
changed, err := hosts.Save(p.Name, entities)
199193
if err != nil && firstTime {
194+
// Permission denied - retry with sudo
200195
args := []string{"--", "devbox", "update-hosts", "--name", p.Name}
201196
if cleanup {
202197
args = append(args, "--cleanup")
203198
}
204199

200+
fmt.Println("[*] Update hosts file...")
205201
cmd := exec.Command("sudo", args...)
206202
if err := cmd.Run(); err != nil {
207203
return fmt.Errorf("failed to save hosts file: %w", err)
208204
}
209205
} else if err != nil {
210206
return fmt.Errorf("failed to save hosts file: %w", err)
207+
} else if changed {
208+
fmt.Println("[*] Updated hosts file")
211209
}
212210

213-
p.HasHosts = len(entities) == 0
214-
return p.SaveState()
211+
return nil
215212
}

internal/cert/cert_darwin.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cert
66
import (
77
"bytes"
88
"crypto/x509"
9+
"encoding/pem"
910
"fmt"
1011
"os/exec"
1112
)
@@ -18,12 +19,31 @@ func (c *cert) isSynced() bool {
1819
return false
1920
}
2021

21-
certFromKeychain, err := decodePEM[x509.Certificate](outBuf.Bytes())
22-
if err != nil {
23-
return false
22+
// The command may return multiple PEM blocks if there are multiple
23+
// certificates with matching names. We need to check all of them.
24+
data := outBuf.Bytes()
25+
for len(data) > 0 {
26+
block, rest := pem.Decode(data)
27+
if block == nil {
28+
break
29+
}
30+
data = rest
31+
32+
if block.Type != pemTypeCertificate {
33+
continue
34+
}
35+
36+
cert, err := x509.ParseCertificate(block.Bytes)
37+
if err != nil {
38+
continue
39+
}
40+
41+
if c.cert.Equal(cert) {
42+
return true
43+
}
2444
}
2545

26-
return c.cert.Equal(certFromKeychain)
46+
return false
2747
}
2848

2949
func (c *cert) installCA() error {

internal/hosts/hosts.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,25 @@ const (
1414
endMarkerTemplate = "# END: Devbox: '%s' project"
1515
)
1616

17-
func Save(projectName string, entries []string) error {
17+
// Save updates the hosts file with the given entries. Returns (changed, error).
18+
// If no changes were needed, changed is false and no write occurs.
19+
func Save(projectName string, entries []string) (bool, error) {
1820
return save(defaultHostFile, projectName, entries)
1921
}
2022

21-
func save(hostFile, projectName string, entries []string) error {
23+
func save(hostFile, projectName string, entries []string) (bool, error) {
2224
markerBegin := fmt.Sprintf(beginMarkerTemplate, projectName)
2325
markerEnd := fmt.Sprintf(endMarkerTemplate, projectName)
2426

2527
fileInfo, err := os.Stat(hostFile)
2628
if err != nil {
27-
return fmt.Errorf("failed to stat hosts file: %w", err)
29+
return false, fmt.Errorf("failed to stat hosts file: %w", err)
2830
}
2931
fileMode := fileInfo.Mode()
3032

3133
oldContent, err := os.ReadFile(hostFile)
3234
if err != nil {
33-
return fmt.Errorf("failed to read hosts file: %w", err)
35+
return false, fmt.Errorf("failed to read hosts file: %w", err)
3436
}
3537

3638
var newContent strings.Builder
@@ -68,10 +70,10 @@ func save(hostFile, projectName string, entries []string) error {
6870
}
6971

7072
if lookupForEnd {
71-
return fmt.Errorf("unexpected end of file")
73+
return false, fmt.Errorf("unexpected end of file")
7274
}
7375

74-
if !replaced {
76+
if !replaced && len(entries) > 0 {
7577
newContent.WriteString(markerBegin + "\n")
7678
for _, entry := range entries {
7779
newContent.WriteString(entry + "\n")
@@ -80,8 +82,12 @@ func save(hostFile, projectName string, entries []string) error {
8082
}
8183

8284
if newContent.String() == string(oldContent) {
83-
return nil
85+
return false, nil
8486
}
8587

86-
return os.WriteFile(hostFile, []byte(newContent.String()), fileMode)
88+
err = os.WriteFile(hostFile, []byte(newContent.String()), fileMode)
89+
if err != nil {
90+
return false, fmt.Errorf("failed to write hosts file: %w", err)
91+
}
92+
return true, nil
8793
}

internal/hosts/hosts_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ func TestSave(t *testing.T) {
4949
err = os.WriteFile(tempFile.Name(), []byte(tc.initialContent), 0644)
5050
assert.NoError(t, err)
5151

52-
err = save(tempFile.Name(), tc.projectName, tc.entries)
53-
52+
_, err = save(tempFile.Name(), tc.projectName, tc.entries)
5453
assert.NoError(t, err)
5554

5655
content, err := os.ReadFile(tempFile.Name())

internal/project/project.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88
"os"
99
"path/filepath"
10+
"slices"
1011
"strings"
1112
"unicode"
1213

@@ -29,7 +30,6 @@ type Project struct {
2930
CertConfig CertConfig
3031

3132
LocalMounts map[string]string // some service's full mount path -> local path
32-
HasHosts bool
3333

3434
envFiles []string
3535
}
@@ -119,8 +119,7 @@ func (p *Project) WithSelectedServices(names []string, options ...types.Dependen
119119

120120
func (p *Project) SaveState() error {
121121
state := &stateFileStruct{
122-
Mounts: p.LocalMounts,
123-
HasHosts: p.HasHosts,
122+
Mounts: p.LocalMounts,
124123
}
125124

126125
json, err := json.Marshal(state)
@@ -184,12 +183,10 @@ func loadState(p *Project) error {
184183
return fmt.Errorf("failed to unmarshal state: %w", err)
185184
}
186185

187-
if state.Mounts == nil {
188-
return nil
186+
if state.Mounts != nil {
187+
p.LocalMounts = state.Mounts
189188
}
190189

191-
p.LocalMounts = state.Mounts
192-
193190
return nil
194191
}
195192

@@ -232,9 +229,17 @@ func applyHosts(p *Project) error {
232229
}
233230
}
234231

235-
entities := []string{}
236-
for ip, hosts := range ipToHosts {
237-
entities = append(entities, fmt.Sprintf("%s %s", ip, strings.Join(hosts, " ")))
232+
// Sort IPs to ensure deterministic output order
233+
ips := make([]string, 0, len(ipToHosts))
234+
for ip := range ipToHosts {
235+
ips = append(ips, ip)
236+
}
237+
slices.Sort(ips)
238+
239+
entities := make([]string, 0, len(ips))
240+
for _, ip := range ips {
241+
slices.Sort(ipToHosts[ip])
242+
entities = append(entities, fmt.Sprintf("%s %s", ip, strings.Join(ipToHosts[ip], " ")))
238243
}
239244

240245
p.HostEntities = entities

internal/project/struct.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package project
22

33
type stateFileStruct struct {
4-
Mounts map[string]string `json:"mounts"`
5-
HasHosts bool `json:"hasHosts"`
4+
Mounts map[string]string `json:"mounts"`
65
}

0 commit comments

Comments
 (0)