Skip to content

Commit 86392ce

Browse files
authored
nixcache: automatically configure nix daemon (#1996)
1 parent e2d03b7 commit 86392ce

File tree

7 files changed

+179
-30
lines changed

7 files changed

+179
-30
lines changed

internal/boxcli/cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func cacheConfigureCmd() *cobra.Command {
8181
u, _ := user.Current()
8282
username = u.Username
8383
}
84-
return nixcache.Get().ConfigureAWS(cmd.Context(), username)
84+
return nixcache.Get().Configure(cmd.Context(), username)
8585
},
8686
}
8787
cmd.Flags().StringVar(&username, "user", "", "")

internal/devbox/packages.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"io/fs"
1111
"os"
12+
"os/user"
1213
"path/filepath"
1314
"runtime/trace"
1415
"slices"
@@ -22,6 +23,7 @@ import (
2223
"go.jetpack.io/devbox/internal/devpkg"
2324
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
2425
"go.jetpack.io/devbox/internal/lock"
26+
"go.jetpack.io/devbox/internal/redact"
2527
"go.jetpack.io/devbox/internal/shellgen"
2628

2729
"go.jetpack.io/devbox/internal/boxcli/usererr"
@@ -456,6 +458,25 @@ func (d *Devbox) installNixPackagesToStore(ctx context.Context, mode installMode
456458
if err == nil {
457459
args.Env = creds.Env()
458460
}
461+
462+
u, err := user.Current()
463+
if err != nil {
464+
err = redact.Errorf("lookup current user: %v", err)
465+
debug.Log("error configuring cache: %v", err)
466+
}
467+
err = d.providers.NixCache.Configure(ctx, u.Username)
468+
if err != nil {
469+
debug.Log("error configuring cache: %v", err)
470+
471+
var daemonErr *nix.DaemonError
472+
if errors.As(err, &daemonErr) {
473+
// Error here to give the user a chance to restart the daemon.
474+
return usererr.New("Devbox configured Nix to use %q as a cache. Please restart the Nix daemon and re-run Devbox.", args.ExtraSubstituter)
475+
}
476+
// Other errors indicate we couldn't update nix.conf, so just warn and continue
477+
// by building from source if necessary.
478+
ux.Fwarning(d.stderr, "Devbox was unable to configure Nix to use your organization's private cache. Some packages might be built from source.")
479+
}
459480
}
460481
for _, installable := range installables {
461482
err = nix.Build(ctx, args, installable)

internal/devbox/providers/nixcache/nixcache.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/AlecAivazis/survey/v2"
1414
"go.jetpack.io/devbox/internal/build"
15+
"go.jetpack.io/devbox/internal/debug"
1516
"go.jetpack.io/devbox/internal/devbox/providers/identity"
1617
"go.jetpack.io/devbox/internal/envir"
1718
"go.jetpack.io/devbox/internal/fileutil"
@@ -31,18 +32,30 @@ func Get() *Provider {
3132
return singleton
3233
}
3334

34-
func (p *Provider) ConfigureAWS(ctx context.Context, username string) error {
35+
func (p *Provider) Configure(ctx context.Context, username string) error {
36+
debug.Log("checking if nix cache is configured for %s", username)
37+
3538
rootConfig, err := p.rootAWSConfigPath()
3639
if err != nil {
3740
return err
3841
}
39-
if fileutil.Exists(rootConfig) {
40-
// Already configured.
42+
debug.Log("root aws config path is: %s", rootConfig)
43+
awsConfigExists := fileutil.Exists(rootConfig)
44+
45+
cfg, err := nix.CurrentConfig(ctx)
46+
if err != nil {
47+
return err
48+
}
49+
trusted, _ := cfg.IsUserTrusted(ctx, username)
50+
51+
configured := awsConfigExists && trusted
52+
debug.Log("nix cache configured = %v (awsConfigExists == %v && trusted == %v)", configured, awsConfigExists, trusted)
53+
if configured {
4154
return nil
4255
}
4356

4457
if os.Getuid() == 0 {
45-
err := p.configureRoot(username)
58+
err := p.configureRoot(ctx, username)
4659
if err != nil {
4760
return redact.Errorf("update ~root/.aws/config with devbox credentials: %s", err)
4861
}
@@ -72,7 +85,7 @@ func (p *Provider) rootAWSConfigPath() (string, error) {
7285
return filepath.Join(u.HomeDir, ".aws", "config"), nil
7386
}
7487

75-
func (p *Provider) configureRoot(username string) error {
88+
func (p *Provider) configureRoot(ctx context.Context, username string) error {
7689
exe := p.executable()
7790
if exe == "" {
7891
return redact.Errorf("get path to current devbox executable")
@@ -113,7 +126,14 @@ credential_process = %s -u %s -i %s cache credentials
113126
if err != nil {
114127
return err
115128
}
116-
return config.Close()
129+
if err := config.Close(); err != nil {
130+
return err
131+
}
132+
133+
if err := nix.IncludeDevboxConfig(ctx, username); err != nil {
134+
return redact.Errorf("modify nix config: %v", err)
135+
}
136+
return nil
117137
}
118138

119139
func (p *Provider) sudoConfigureRoot(ctx context.Context, username string) error {
@@ -140,9 +160,14 @@ func (p *Provider) sudoConfigureRoot(ctx context.Context, username string) error
140160
cmd.Stdout = os.Stdout
141161
cmd.Stderr = os.Stderr
142162

163+
debug.Log("running sudo: %s", cmd)
143164
if err := cmd.Run(); err != nil {
144165
return fmt.Errorf("failed to relaunch with sudo: %w", err)
145166
}
167+
168+
// Print a warning if we were unable to automatically make the user
169+
// trusted.
170+
checkIfUserCanAddSubstituter(ctx)
146171
return nil
147172
}
148173

@@ -205,9 +230,6 @@ func (p *Provider) URI(ctx context.Context) (string, error) {
205230
if err != nil {
206231
return "", redact.Errorf("nixcache: get uri: %w", redact.Safe(err))
207232
}
208-
if uri != "" {
209-
checkIfUserCanAddSubstituter(ctx)
210-
}
211233
return uri, nil
212234
}
213235

@@ -227,7 +249,12 @@ func checkIfUserCanAddSubstituter(ctx context.Context) {
227249
if err != nil {
228250
return
229251
}
230-
trusted, _ := cfg.IsUserTrusted(ctx)
252+
253+
u, err := user.Current()
254+
if err != nil {
255+
return
256+
}
257+
trusted, _ := cfg.IsUserTrusted(ctx, u.Username)
231258
if !trusted {
232259
ux.Fwarning(
233260
os.Stderr,

internal/nix/config.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package nix
22

33
import (
4+
"cmp"
45
"context"
56
"encoding/json"
67
"errors"
8+
"fmt"
9+
"io"
10+
"os"
711
"os/exec"
812
"os/user"
13+
"path/filepath"
914
"slices"
1015
"strings"
1116

@@ -51,15 +56,15 @@ func CurrentConfig(ctx context.Context) (Config, error) {
5156
// IsUserTrusted reports if the current OS user is in the trusted-users list. If
5257
// there are any groups in the list, it also checks if the user belongs to any
5358
// of them.
54-
func (c Config) IsUserTrusted(ctx context.Context) (bool, error) {
59+
func (c Config) IsUserTrusted(ctx context.Context, username string) (bool, error) {
5560
trusted := c.TrustedUsers.Value
5661
if len(trusted) == 0 {
5762
return false, nil
5863
}
5964

60-
current, err := user.Current()
65+
current, err := user.Lookup(username)
6166
if err != nil {
62-
return false, redact.Errorf("lookup current user: %v", err)
67+
return false, redact.Errorf("lookup user: %v", err)
6368
}
6469
if slices.Contains(trusted, current.Username) {
6570
return true, nil
@@ -99,3 +104,66 @@ func (c Config) IsUserTrusted(ctx context.Context) (bool, error) {
99104
}
100105
return false, nil
101106
}
107+
108+
func IncludeDevboxConfig(ctx context.Context, username string) error {
109+
info, _ := versionInfo()
110+
path := cmp.Or(info.SystemConfig, "/etc/nix/nix.conf")
111+
includePath := filepath.Join(filepath.Dir(path), "devbox-nix.conf")
112+
b := fmt.Appendf(nil, "# This config was auto-generated by Devbox.\n\nextra-trusted-users = %s\n", username)
113+
if err := os.WriteFile(includePath, b, 0o664); err != nil {
114+
return redact.Errorf("write devbox nix.conf: %v", err)
115+
}
116+
117+
appended, err := appendConfigInclude(path, includePath)
118+
if err != nil {
119+
return err
120+
}
121+
if appended {
122+
return restartDaemon(ctx)
123+
}
124+
return nil
125+
}
126+
127+
func appendConfigInclude(srcPath, includePath string) (appended bool, err error) {
128+
nixConf, err := os.OpenFile(srcPath, os.O_RDWR, 0)
129+
if err != nil {
130+
return false, err
131+
}
132+
defer nixConf.Close()
133+
134+
confb, err := io.ReadAll(nixConf)
135+
if err != nil {
136+
return false, err
137+
}
138+
for _, line := range strings.Split(string(confb), "\n") {
139+
line = strings.TrimSpace(line)
140+
if line == "" {
141+
// <whitespace>
142+
continue
143+
}
144+
if strings.HasPrefix(line, "#") {
145+
// # comment
146+
continue
147+
}
148+
149+
path := strings.TrimSpace(strings.TrimPrefix(line, "include"))
150+
if path == includePath {
151+
// include devbox-nix.conf
152+
return false, nil
153+
}
154+
path = strings.TrimSpace(strings.TrimPrefix(line, "!include"))
155+
if path == includePath {
156+
// !include devbox-nix.conf
157+
return false, nil
158+
}
159+
}
160+
161+
include := "\ninclude " + includePath + "\n"
162+
if _, err := nixConf.WriteString(include); err != nil {
163+
return false, redact.Errorf("append %q to %s: %v", redact.Safe(include), srcPath, err)
164+
}
165+
if err := nixConf.Close(); err != nil {
166+
return false, redact.Errorf("append %q to %s: %v", redact.Safe(include), srcPath, err)
167+
}
168+
return true, nil
169+
}

internal/nix/config_test.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,21 @@ import (
88

99
//nolint:revive
1010
func TestConfigIsUserTrusted(t *testing.T) {
11+
curUser, err := user.Current()
12+
if err != nil {
13+
t.Fatal("lookup current user:", err)
14+
}
15+
1116
t.Run("UsernameInList", func(t *testing.T) {
12-
u, err := user.Current()
13-
if err != nil {
14-
t.Fatal(err)
15-
}
16-
t.Setenv("NIX_CONFIG", "trusted-users = "+u.Username)
17+
t.Setenv("NIX_CONFIG", "trusted-users = "+curUser.Username)
1718

1819
ctx := context.Background()
1920
cfg, err := CurrentConfig(ctx)
2021
if err != nil {
2122
t.Fatal(err)
2223
}
2324

24-
trusted, err := cfg.IsUserTrusted(ctx)
25+
trusted, err := cfg.IsUserTrusted(ctx, curUser.Username)
2526
if err != nil {
2627
t.Fatal(err)
2728
}
@@ -30,11 +31,7 @@ func TestConfigIsUserTrusted(t *testing.T) {
3031
}
3132
})
3233
t.Run("UserGroupInList", func(t *testing.T) {
33-
u, err := user.Current()
34-
if err != nil {
35-
t.Fatal(err)
36-
}
37-
g, err := user.LookupGroupId(u.Gid)
34+
g, err := user.LookupGroupId(curUser.Gid)
3835
if err != nil {
3936
t.Fatal(err)
4037
}
@@ -46,7 +43,7 @@ func TestConfigIsUserTrusted(t *testing.T) {
4643
t.Fatal(err)
4744
}
4845

49-
trusted, err := cfg.IsUserTrusted(ctx)
46+
trusted, err := cfg.IsUserTrusted(ctx, curUser.Username)
5047
if err != nil {
5148
t.Fatal(err)
5249
}
@@ -63,7 +60,7 @@ func TestConfigIsUserTrusted(t *testing.T) {
6360
t.Fatal(err)
6461
}
6562

66-
trusted, err := cfg.IsUserTrusted(ctx)
63+
trusted, err := cfg.IsUserTrusted(ctx, curUser.Username)
6764
if err != nil {
6865
t.Fatal(err)
6966
}
@@ -80,7 +77,7 @@ func TestConfigIsUserTrusted(t *testing.T) {
8077
t.Fatal(err)
8178
}
8279

83-
trusted, err := cfg.IsUserTrusted(ctx)
80+
trusted, err := cfg.IsUserTrusted(ctx, curUser.Username)
8481
if err != nil {
8582
t.Fatal(err)
8683
}
@@ -97,7 +94,7 @@ func TestConfigIsUserTrusted(t *testing.T) {
9794
t.Fatal(err)
9895
}
9996

100-
trusted, err := cfg.IsUserTrusted(ctx)
97+
trusted, err := cfg.IsUserTrusted(ctx, curUser.Username)
10198
if err != nil {
10299
t.Fatal(err)
103100
}

internal/nix/nix.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"os/exec"
1313
"path/filepath"
1414
"regexp"
15+
"runtime"
1516
"runtime/trace"
1617
"strings"
1718
"sync"
@@ -412,3 +413,35 @@ func parseInsecurePackagesFromExitError(errorMsg string) []string {
412413

413414
return insecurePackages
414415
}
416+
417+
var ErrUnknownServiceManager = errors.New("unknown service manager")
418+
419+
func restartDaemon(ctx context.Context) error {
420+
if runtime.GOOS != "darwin" {
421+
err := fmt.Errorf("don't know how to restart nix daemon: %w", ErrUnknownServiceManager)
422+
return &DaemonError{err: err}
423+
}
424+
425+
cmd := exec.CommandContext(ctx, "launchctl", "bootout", "system", "/Library/LaunchDaemons/org.nixos.nix-daemon.plist")
426+
out, err := cmd.CombinedOutput()
427+
if err != nil {
428+
return &DaemonError{
429+
cmd: cmd.String(),
430+
stderr: out,
431+
err: fmt.Errorf("stop nix daemon: %w", err),
432+
}
433+
}
434+
cmd = exec.CommandContext(ctx, "launchctl", "bootstrap", "system", "/Library/LaunchDaemons/org.nixos.nix-daemon.plist")
435+
out, err = cmd.CombinedOutput()
436+
if err != nil {
437+
return &DaemonError{
438+
cmd: cmd.String(),
439+
stderr: out,
440+
err: fmt.Errorf("start nix daemon: %w", err),
441+
}
442+
}
443+
444+
// TODO(gcurtis): poll for daemon to come back instead.
445+
time.Sleep(2 * time.Second)
446+
return nil
447+
}

internal/nix/store.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ func (e *DaemonError) Unwrap() error {
112112
func (e *DaemonError) Redact() string {
113113
// Don't include e.stderr in redacted messages because it can contain
114114
// things like paths and usernames.
115-
return fmt.Sprintf("command %s: %s", e.cmd, e.err)
115+
if e.cmd != "" {
116+
return fmt.Sprintf("command %s: %s", e.cmd, e.err)
117+
}
118+
return e.err.Error()
116119
}
117120

118121
// DaemonVersion returns the version of the currently running Nix daemon.

0 commit comments

Comments
 (0)