Skip to content

Commit 1a31e20

Browse files
authored
internal/setup: move sudo into SudoDevbox function + fix macOS CI (#2043)
1 parent a592fe5 commit 1a31e20

File tree

5 files changed

+317
-134
lines changed

5 files changed

+317
-134
lines changed

internal/devbox/packages.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ func (d *Devbox) installNixPackagesToStore(ctx context.Context, mode installMode
469469
}
470470
// Other errors indicate we couldn't update nix.conf, so just warn and continue
471471
// by building from source if necessary.
472-
ux.Fwarning(d.stderr, "Devbox was unable to configure Nix to use your organization's private cache. Some packages might be built from source.")
472+
ux.Fwarning(d.stderr, "Devbox was unable to configure Nix to use your organization's private cache. Some packages might be built from source.\n")
473473
}
474474
}
475475

internal/devbox/providers/nixcache/nixcache.go

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@ package nixcache
22

33
import (
44
"context"
5-
"errors"
6-
"os"
75
"time"
86

97
"go.jetpack.io/devbox/internal/build"
108
"go.jetpack.io/devbox/internal/cachehash"
119
"go.jetpack.io/devbox/internal/devbox/providers/identity"
1210
"go.jetpack.io/devbox/internal/redact"
13-
"go.jetpack.io/devbox/internal/setup"
1411
"go.jetpack.io/pkg/api"
1512
nixv1alpha1 "go.jetpack.io/pkg/api/gen/priv/nix/v1alpha1"
1613
"go.jetpack.io/pkg/auth/session"
@@ -25,54 +22,6 @@ func Get() *Provider {
2522
return singleton
2623
}
2724

28-
func (p *Provider) Configure(ctx context.Context, username string) error {
29-
return p.configure(ctx, username, false)
30-
}
31-
32-
func (p *Provider) ConfigureReprompt(ctx context.Context, username string) error {
33-
return p.configure(ctx, username, true)
34-
}
35-
36-
func (p *Provider) configure(ctx context.Context, username string, reprompt bool) error {
37-
setupTasks := []struct {
38-
key string
39-
task setup.Task
40-
}{
41-
{"nixcache-setup-aws", &awsSetupTask{username}},
42-
{"nixcache-setup-nix", &nixSetupTask{username}},
43-
}
44-
if reprompt {
45-
for _, t := range setupTasks {
46-
setup.Reset(t.key)
47-
}
48-
}
49-
50-
// If we're already root, then do the setup without prompting the user
51-
// for confirmation.
52-
if os.Getuid() == 0 {
53-
for _, t := range setupTasks {
54-
err := setup.Run(ctx, t.key, t.task)
55-
if err != nil {
56-
return redact.Errorf("nixcache: run setup: %v", err)
57-
}
58-
}
59-
return nil
60-
}
61-
62-
// Otherwise, ask the user to confirm if it's okay to sudo.
63-
const sudoPrompt = "Devbox requires root to configure the Nix daemon to use your organization's Devbox cache. Allow sudo?"
64-
for _, t := range setupTasks {
65-
err := setup.ConfirmRun(ctx, t.key, t.task, sudoPrompt)
66-
if errors.Is(err, setup.ErrUserRefused) {
67-
return nil
68-
}
69-
if err != nil {
70-
return redact.Errorf("nixcache: run setup: %v", err)
71-
}
72-
}
73-
return nil
74-
}
75-
7625
// Credentials fetches short-lived credentials that grant access to the user's
7726
// private cache.
7827
func (p *Provider) Credentials(ctx context.Context) (AWSCredentials, error) {

internal/devbox/providers/nixcache/setup.go

Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,83 +9,96 @@ import (
99
"os/exec"
1010
"os/user"
1111
"path/filepath"
12+
"strings"
1213
"time"
14+
"unicode"
1315

1416
"go.jetpack.io/devbox/internal/debug"
1517
"go.jetpack.io/devbox/internal/envir"
1618
"go.jetpack.io/devbox/internal/nix"
1719
"go.jetpack.io/devbox/internal/redact"
1820
"go.jetpack.io/devbox/internal/setup"
19-
"go.jetpack.io/devbox/internal/xdg"
2021
)
2122

22-
// nixSetupTask adds the user to Nix's trusted-users list so that they can use
23-
// their private Devbox cache with the Nix daemon.
24-
type nixSetupTask struct {
25-
// username is the OS username to trust.
26-
username string
23+
func (p *Provider) Configure(ctx context.Context, username string) error {
24+
return p.configure(ctx, username, false)
2725
}
2826

29-
func (n *nixSetupTask) NeedsRun(ctx context.Context, lastRun setup.RunInfo) bool {
30-
cfg, err := nix.CurrentConfig(ctx)
31-
if err != nil {
32-
return true
33-
}
34-
trusted, _ := cfg.IsUserTrusted(ctx, n.username)
35-
if trusted {
36-
debug.Log("nixcache: skipping setup task nixcache-setup-nix: user %s is already trusted", n.username)
37-
return false
38-
}
39-
40-
if _, err := nix.DaemonVersion(ctx); err != nil {
41-
// This looks like a single-user install, so no need to
42-
// configure the daemon.
43-
debug.Log("nixcache: skipping setup task nixcache-setup-nix: error connecting to nix daemon, assuming single-user install: %v", err)
44-
return false
45-
}
46-
return true
27+
func (p *Provider) ConfigureReprompt(ctx context.Context, username string) error {
28+
return p.configure(ctx, username, true)
4729
}
4830

49-
func (n *nixSetupTask) Run(ctx context.Context) error {
50-
if os.Getuid() != 0 {
51-
return sudo(ctx, n.username)
31+
func (p *Provider) configure(ctx context.Context, username string, reprompt bool) error {
32+
const key = "nixcache-setup"
33+
if reprompt {
34+
setup.Reset(key)
5235
}
53-
err := nix.IncludeDevboxConfig(ctx, n.username)
54-
if err != nil {
55-
return redact.Errorf("modify nix config: %v", err)
36+
37+
task := &setupTask{username}
38+
const sudoPrompt = "You're logged into a Devbox account that now has access to a Nix cache. " +
39+
"Allow Devbox to configure Nix to use the new cache (requires sudo)?"
40+
err := setup.ConfirmRun(ctx, key, task, sudoPrompt)
41+
if err != nil && !errors.Is(err, setup.ErrUserRefused) {
42+
return redact.Errorf("nixcache: run setup: %v", err)
5643
}
5744
return nil
5845
}
5946

60-
// awsSetupTask configures the OS's root account to authenticate with AWS by
61-
// obtaining a token from `devbox cache credentials`.
62-
type awsSetupTask struct {
63-
// username is the OS username that the Nix daemon should sudo as when
64-
// running `devbox cache credentials`.
47+
// setupTask adds the user to Nix's trusted-users list and updates
48+
// ~root/.aws/config so that they can use their Devbox cache with the
49+
// Nix daemon.
50+
type setupTask struct {
51+
// username is the OS username to trust.
6552
username string
6653
}
6754

68-
func (a *awsSetupTask) NeedsRun(ctx context.Context, lastRun setup.RunInfo) bool {
69-
// This task only needs to run once.
70-
if !lastRun.Time.IsZero() {
71-
debug.Log("nixcache: skipping setup task nixcache-setup-aws: setup was already run at %s", lastRun.Time)
55+
func (s *setupTask) NeedsRun(ctx context.Context, lastRun setup.RunInfo) bool {
56+
if _, err := nix.DaemonVersion(ctx); err != nil {
57+
// This looks like a single-user install, so no need to
58+
// configure the daemon or root's AWS credentials.
59+
debug.Log("nixcache: skipping setup: error connecting to nix daemon, assuming single-user install: %v", err)
7260
return false
7361
}
7462

75-
// No need to configure the daemon if this looks like a single-user
76-
// install.
77-
if _, err := nix.DaemonVersion(ctx); err != nil {
78-
debug.Log("nixcache: skipping setup task nixcache-setup-aws: error connecting to nix daemon, assuming single-user install: %v", err)
79-
return false
63+
if lastRun.Time.IsZero() {
64+
debug.Log("nixcache: running setup: first time setup")
65+
return true
66+
}
67+
cfg, err := nix.CurrentConfig(ctx)
68+
if err != nil {
69+
debug.Log("nixcache: running setup: error getting current nix config, assuming user %s isn't trusted", s.username)
70+
return true
8071
}
81-
return true
72+
trusted, err := cfg.IsUserTrusted(ctx, s.username)
73+
if err != nil {
74+
debug.Log("nixcache: running setup: error checking if user %s is trusted, assuming they aren't", s.username)
75+
return true
76+
}
77+
if !trusted {
78+
debug.Log("nixcache: running setup: user %s isn't trusted", s.username)
79+
return true
80+
}
81+
return false
8282
}
8383

84-
func (a *awsSetupTask) Run(ctx context.Context) error {
85-
if os.Getuid() != 0 {
86-
return sudo(ctx, a.username)
84+
func (s *setupTask) Run(ctx context.Context) error {
85+
ran, err := setup.SudoDevbox(ctx, "cache", "configure", "--user", s.username)
86+
if ran || err != nil {
87+
return err
88+
}
89+
90+
err = nix.IncludeDevboxConfig(ctx, s.username)
91+
if err != nil {
92+
return redact.Errorf("update nix config: %v", err)
8793
}
94+
err = s.updateAWSConfig()
95+
if err != nil {
96+
return redact.Errorf("update root aws config: %v", err)
97+
}
98+
return nil
99+
}
88100

101+
func (s *setupTask) updateAWSConfig() error {
89102
exe, err := devboxExecutable()
90103
if err != nil {
91104
return err
@@ -133,8 +146,8 @@ func (a *awsSetupTask) Run(ctx context.Context) error {
133146
[default]
134147
# sudo as the configured user so that their cached credential files have the
135148
# correct ownership.
136-
credential_process = %s -u %s -i -- %s cache credentials
137-
`, header, sudo, a.username, exe)
149+
credential_process = %s -u %s -i %s-- %s cache credentials
150+
`, header, sudo, s.username, propagatedEnv(), exe)
138151
if err != nil {
139152
return redact.Errorf("write to ~root/.aws/config: %v", err)
140153
}
@@ -144,35 +157,51 @@ credential_process = %s -u %s -i -- %s cache credentials
144157
return nil
145158
}
146159

147-
func sudo(ctx context.Context, username string) error {
148-
// Use the absolute path to Devbox instead of relying on PATH for two
149-
// reasons:
150-
//
151-
// 1. sudo isn't guaranteed to preserve the current PATH and the root
152-
// user might not have devbox in its PATH.
153-
// 2. If we're running an alternative version of Devbox
154-
// (such as a dev build) we want to use the same binary.
155-
exe, err := devboxExecutable()
156-
if err != nil {
157-
return err
158-
}
159-
160-
// Ensure the XDG state directory exists before sudoing, otherwise it
161-
// will be owned by root. It's used by the setup package to remember
162-
// user responses to the confirmation prompt.
163-
err = os.MkdirAll(xdg.StateSubpath("devbox"), 0o700)
164-
if err != nil {
165-
return err
160+
// propagatedEnv returns a string of space-separated VAR=value pairs of
161+
// environment variables that should be propagated to the credential_process
162+
// command in ~root/.aws/config. This is especially important for CI because the
163+
// Nix daemon won't otherwise see any environment variables set by the job.
164+
func propagatedEnv() string {
165+
envs := []string{
166+
"DEVBOX_API_TOKEN",
167+
"DEVBOX_PROD",
168+
"DEVBOX_USE_VERSION",
169+
"XDG_CACHE_HOME",
170+
"XDG_CONFIG_DIRS",
171+
"XDG_CONFIG_HOME",
172+
"XDG_DATA_DIRS",
173+
"XDG_DATA_HOME",
174+
"XDG_RUNTIME_DIR",
175+
"XDG_STATE_HOME",
166176
}
177+
strb := strings.Builder{}
178+
for _, name := range envs {
179+
val := os.Getenv(name)
180+
if val == "" {
181+
continue
182+
}
183+
notPrintable := strings.ContainsFunc(val, func(r rune) bool {
184+
return !unicode.IsPrint(r)
185+
})
186+
if notPrintable {
187+
debug.Log("nixcache: not including environment variable in ~root/.aws/config because it contains nonprintable runes: %q=%q", name, val)
188+
continue
189+
}
167190

168-
cmd := exec.CommandContext(ctx, "sudo", "--preserve-env=XDG_STATE_HOME", "--", exe, "cache", "configure", "--user", username)
169-
cmd.Stdin = os.Stdin
170-
cmd.Stdout = os.Stdout
171-
cmd.Stderr = os.Stderr
172-
if err := cmd.Run(); err != nil {
173-
return fmt.Errorf("relaunch with sudo: %w", err)
191+
strb.WriteString(name)
192+
strb.WriteString(`="`)
193+
for _, r := range val {
194+
switch r {
195+
// Special characters inside double quotes:
196+
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
197+
case '$', '`', '"', '\\':
198+
strb.WriteByte('\\')
199+
}
200+
strb.WriteRune(r)
201+
}
202+
strb.WriteString(`" `)
174203
}
175-
return nil
204+
return strb.String()
176205
}
177206

178207
// rootAWSConfigPath returns the default AWS config path for the root user. In a

0 commit comments

Comments
 (0)