@@ -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