@@ -9,83 +9,96 @@ import (
9
9
"os/exec"
10
10
"os/user"
11
11
"path/filepath"
12
+ "strings"
12
13
"time"
14
+ "unicode"
13
15
14
16
"go.jetpack.io/devbox/internal/debug"
15
17
"go.jetpack.io/devbox/internal/envir"
16
18
"go.jetpack.io/devbox/internal/nix"
17
19
"go.jetpack.io/devbox/internal/redact"
18
20
"go.jetpack.io/devbox/internal/setup"
19
- "go.jetpack.io/devbox/internal/xdg"
20
21
)
21
22
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 )
27
25
}
28
26
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 )
47
29
}
48
30
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 )
52
35
}
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 )
56
43
}
57
44
return nil
58
45
}
59
46
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 .
65
52
username string
66
53
}
67
54
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 )
72
60
return false
73
61
}
74
62
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
80
71
}
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
82
82
}
83
83
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 )
87
93
}
94
+ err = s .updateAWSConfig ()
95
+ if err != nil {
96
+ return redact .Errorf ("update root aws config: %v" , err )
97
+ }
98
+ return nil
99
+ }
88
100
101
+ func (s * setupTask ) updateAWSConfig () error {
89
102
exe , err := devboxExecutable ()
90
103
if err != nil {
91
104
return err
@@ -133,8 +146,8 @@ func (a *awsSetupTask) Run(ctx context.Context) error {
133
146
[default]
134
147
# sudo as the configured user so that their cached credential files have the
135
148
# 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 )
138
151
if err != nil {
139
152
return redact .Errorf ("write to ~root/.aws/config: %v" , err )
140
153
}
@@ -144,35 +157,51 @@ credential_process = %s -u %s -i -- %s cache credentials
144
157
return nil
145
158
}
146
159
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" ,
166
176
}
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
+ }
167
190
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 (`" ` )
174
203
}
175
- return nil
204
+ return strb . String ()
176
205
}
177
206
178
207
// rootAWSConfigPath returns the default AWS config path for the root user. In a
0 commit comments