Skip to content

Commit 7a67909

Browse files
authored
nixcache: improve auto-configuration of cache (#2010)
Fix a few issues with the current auto-configuration flow for private nix caches: - Track whether or not we've configured ~root/.aws/config in a separate state file in the user's home directory. This lets Devbox know if AWS has already been configured, even if it cannot read root's home directory. - If the user answers no to the sudo confirmation prompt, don't ask them again. This is so we don't pester the user every time they install a package. - Preserve XDG_STATE_HOME in sudo so that we write state files to the correct directory. - Append a timestamp to the ~root/.aws directory when we back it up. This allows multiple backups if the setup process is run more than once. The logic for saving state around whether or not the cache setup has already run lives in a new `setup` package. The `setup` package tracks when a task last ran, what version of Devbox it ran with, and if there was an error. This makes it easier to define tasks that only run once for a user or only occur after an upgrade.
1 parent f11608f commit 7a67909

File tree

5 files changed

+616
-178
lines changed

5 files changed

+616
-178
lines changed

internal/boxcli/cache.go

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

internal/devbox/providers/nixcache/nixcache.go

Lines changed: 35 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,14 @@ package nixcache
22

33
import (
44
"context"
5-
"fmt"
6-
"io/fs"
5+
"errors"
76
"os"
8-
"os/exec"
9-
"os/user"
10-
"path/filepath"
117
"time"
128

13-
"github.com/AlecAivazis/survey/v2"
149
"go.jetpack.io/devbox/internal/build"
15-
"go.jetpack.io/devbox/internal/debug"
1610
"go.jetpack.io/devbox/internal/devbox/providers/identity"
17-
"go.jetpack.io/devbox/internal/envir"
18-
"go.jetpack.io/devbox/internal/fileutil"
19-
"go.jetpack.io/devbox/internal/nix"
2011
"go.jetpack.io/devbox/internal/redact"
21-
"go.jetpack.io/devbox/internal/ux"
12+
"go.jetpack.io/devbox/internal/setup"
2213
"go.jetpack.io/pkg/api"
2314
nixv1alpha1 "go.jetpack.io/pkg/api/gen/priv/nix/v1alpha1"
2415
"go.jetpack.io/pkg/filecache"
@@ -33,154 +24,53 @@ func Get() *Provider {
3324
}
3425

3526
func (p *Provider) Configure(ctx context.Context, username string) error {
36-
debug.Log("checking if nix cache is configured for %s", username)
37-
38-
rootConfig, err := p.rootAWSConfigPath()
39-
if err != nil {
40-
return err
41-
}
42-
debug.Log("root aws config path is: %s", rootConfig)
43-
awsConfigExists := fileutil.Exists(rootConfig)
27+
return p.configure(ctx, username, false)
28+
}
4429

45-
cfg, err := nix.CurrentConfig(ctx)
46-
if err != nil {
47-
return err
48-
}
49-
trusted, _ := cfg.IsUserTrusted(ctx, username)
30+
func (p *Provider) ConfigureReprompt(ctx context.Context, username string) error {
31+
return p.configure(ctx, username, true)
32+
}
5033

51-
configured := awsConfigExists && trusted
52-
debug.Log("nix cache configured = %v (awsConfigExists == %v && trusted == %v)", configured, awsConfigExists, trusted)
53-
if configured {
54-
return nil
34+
func (p *Provider) configure(ctx context.Context, username string, reprompt bool) error {
35+
setupTasks := []struct {
36+
key string
37+
task setup.Task
38+
}{
39+
{"nixcache-setup-aws", &awsSetupTask{username}},
40+
{"nixcache-setup-nix", &nixSetupTask{username}},
41+
}
42+
if reprompt {
43+
for _, t := range setupTasks {
44+
setup.Reset(t.key)
45+
}
5546
}
5647

48+
// If we're already root, then do the setup without prompting the user
49+
// for confirmation.
5750
if os.Getuid() == 0 {
58-
err := p.configureRoot(ctx, username)
59-
if err != nil {
60-
return redact.Errorf("update ~root/.aws/config with devbox credentials: %s", err)
51+
for _, t := range setupTasks {
52+
err := setup.Run(ctx, t.key, t.task)
53+
if err != nil {
54+
return redact.Errorf("nixcache: run setup: %v", err)
55+
}
6156
}
6257
return nil
6358
}
6459

65-
_, err = nix.DaemonVersion(ctx)
66-
if err == nil {
67-
// It looks like this is a multi-user install running a Nix
68-
// daemon, so we need to configure AWS S3 authentication for the
69-
// root user.
70-
if err := p.sudoConfigureRoot(ctx, username); err != nil {
71-
return err
60+
// Otherwise, ask the user to confirm if it's okay to sudo.
61+
const sudoPrompt = "Devbox requires root to configure the Nix daemon to use your organization's Devbox cache. Allow sudo?"
62+
for _, t := range setupTasks {
63+
err := setup.ConfirmRun(ctx, t.key, t.task, sudoPrompt)
64+
if errors.Is(err, setup.ErrUserRefused) {
65+
return nil
66+
}
67+
if err != nil {
68+
return redact.Errorf("nixcache: run setup: %v", err)
7269
}
7370
}
7471
return nil
7572
}
7673

77-
func (p *Provider) rootAWSConfigPath() (string, error) {
78-
u, err := user.LookupId("0")
79-
if err != nil {
80-
return "", redact.Errorf("lookup root user: %s", err)
81-
}
82-
if u.HomeDir == "" {
83-
return "", redact.Errorf("empty root user home directory: %s", u.Username, err)
84-
}
85-
return filepath.Join(u.HomeDir, ".aws", "config"), nil
86-
}
87-
88-
func (p *Provider) configureRoot(ctx context.Context, username string) error {
89-
exe := p.executable()
90-
if exe == "" {
91-
return redact.Errorf("get path to current devbox executable")
92-
}
93-
sudo, err := exec.LookPath("sudo")
94-
if err != nil {
95-
return redact.Errorf("get path to sudo executable: %s", err)
96-
}
97-
path, err := p.rootAWSConfigPath()
98-
if err != nil {
99-
return err
100-
}
101-
102-
// Rename the .aws directory in case it already exists. We should
103-
// improve this to be more careful with existing ~root/.aws/configs, but
104-
// this seems rare enough that it should be okay for the initial
105-
// implementation.
106-
dir := filepath.Dir(path)
107-
_ = os.Rename(dir, dir+".bak") // ignore errors for non-existent dir
108-
_ = os.Mkdir(dir, 0o755) // ignore errors for dir exists (don't os.MkdirAll the home directory)
109-
110-
config, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.FileMode(0o644))
111-
if err != nil {
112-
return err
113-
}
114-
defer config.Close()
115-
116-
// TODO(gcurtis): it would be nice to use a non-default profile if
117-
// https://github.com/NixOS/nix/issues/5525 ever gets fixed.
118-
_, err = fmt.Fprintf(config, `# This file was generated by Devbox.
119-
# Any overwritten configs can be found in the .aws.bak directory.
120-
121-
[default]
122-
# sudo as the configured user so that their cached credential files have the
123-
# correct ownership.
124-
credential_process = %s -u %s -i %s cache credentials
125-
`, sudo, username, exe)
126-
if err != nil {
127-
return err
128-
}
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
137-
}
138-
139-
func (p *Provider) sudoConfigureRoot(ctx context.Context, username string) error {
140-
// TODO(gcurtis): save the user's response so that we don't pester them
141-
// every time if it's a no.
142-
prompt := &survey.Confirm{
143-
Message: "Devbox requires root to configure the Nix daemon to use your organization's private cache. Allow sudo?",
144-
}
145-
ok := false
146-
if err := survey.AskOne(prompt, &ok); err != nil {
147-
return err
148-
}
149-
if !ok {
150-
return nil
151-
}
152-
153-
exe := p.executable()
154-
if exe == "" {
155-
return redact.Errorf("get path to current devbox executable")
156-
}
157-
158-
cmd := exec.CommandContext(ctx, "sudo", exe, "cache", "configure", "--user", username)
159-
cmd.Stdin = os.Stdin
160-
cmd.Stdout = os.Stdout
161-
cmd.Stderr = os.Stderr
162-
163-
debug.Log("running sudo: %s", cmd)
164-
if err := cmd.Run(); err != nil {
165-
return fmt.Errorf("failed to relaunch with sudo: %w", err)
166-
}
167-
168-
// Print a warning if we were unable to automatically make the user
169-
// trusted.
170-
checkIfUserCanAddSubstituter(ctx)
171-
return nil
172-
}
173-
174-
func (*Provider) executable() string {
175-
if exe := os.Getenv(envir.LauncherPath); exe != "" {
176-
return exe
177-
}
178-
if exe, err := os.Executable(); err == nil {
179-
return exe
180-
}
181-
return ""
182-
}
183-
18474
// Credentials fetches short-lived credentials that grant access to the user's
18575
// private cache.
18676
func (p *Provider) Credentials(ctx context.Context) (AWSCredentials, error) {
@@ -250,38 +140,6 @@ func (p *Provider) URI(ctx context.Context) (string, error) {
250140
return uri, nil
251141
}
252142

253-
func checkIfUserCanAddSubstituter(ctx context.Context) {
254-
// we need to ensure that the user can actually use the extra
255-
// substituter. If the user did a root install, then we need to add
256-
// the trusted user/substituter to the nix.conf file and restart the daemon.
257-
258-
// This check is not perfect, so we still try to use the substituter even if
259-
// it fails
260-
261-
// TODOs:
262-
// * Also check if cache is enabled in nix.conf
263-
// * Test on single user install
264-
// * Automate making user trusted if needed
265-
cfg, err := nix.CurrentConfig(ctx)
266-
if err != nil {
267-
return
268-
}
269-
270-
u, err := user.Current()
271-
if err != nil {
272-
return
273-
}
274-
trusted, _ := cfg.IsUserTrusted(ctx, u.Username)
275-
if !trusted {
276-
ux.Fwarning(
277-
os.Stderr,
278-
"In order to use a custom nix cache you must be a trusted user. Please "+
279-
"add your username to nix.conf (usually located at /etc/nix/nix.conf)"+
280-
" and restart the nix daemon.\n",
281-
)
282-
}
283-
}
284-
285143
// AWSCredentials are short-lived credentials that grant access to a private Nix
286144
// cache in S3. It marshals to JSON per the schema described in
287145
// `aws help config-vars` under "Sourcing Credentials From External Processes".

0 commit comments

Comments
 (0)