From fcca7f72fa47a634fc3fb296bb8e22f117ae4ea9 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Mon, 3 Aug 2026 21:46:31 -0700 Subject: [PATCH] feat!: remove global push/pull to Jetify Cloud `devbox global push` and `devbox global pull` with no argument synced the global profile to a Jetify-hosted S3 bucket, authenticating by exchanging the logged-in ID token for AWS credentials via sts:AssumeRoleWithWebIdentity. Both commands now require an argument and keep working for every source that needs no account: git repos, http(s) archives, plain devbox.json URLs, and local file paths. `push` takes a git repo, `pull` takes a git repo, URL, or file, and cobra rejects the no-argument form instead of falling through to the cloud. devopt.Credentials existed only to carry the ID token into the S3 code and goes away with it. Co-Authored-By: Claude Opus 5 (1M context) --- internal/boxcli/pull.go | 36 +++---------- internal/boxcli/push.go | 28 ++-------- internal/devbox/devopt/devboxopts.go | 12 +---- internal/goutil/goutil.go | 8 --- internal/pullbox/pullbox.go | 41 +++------------ internal/pullbox/s3/config.go | 55 -------------------- internal/pullbox/s3/pull.go | 76 ---------------------------- internal/pullbox/s3/push.go | 64 ----------------------- 8 files changed, 20 insertions(+), 300 deletions(-) delete mode 100644 internal/pullbox/s3/config.go delete mode 100644 internal/pullbox/s3/pull.go delete mode 100644 internal/pullbox/s3/push.go diff --git a/internal/boxcli/pull.go b/internal/boxcli/pull.go index 68e96ae692a..f3001b7ac2f 100644 --- a/internal/boxcli/pull.go +++ b/internal/boxcli/pull.go @@ -12,13 +12,8 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "go.jetify.com/devbox/internal/boxcli/usererr" "go.jetify.com/devbox/internal/devbox" "go.jetify.com/devbox/internal/devbox/devopt" - "go.jetify.com/devbox/internal/devbox/providers/identity" - "go.jetify.com/devbox/internal/goutil" - "go.jetify.com/devbox/internal/pullbox/s3" - "go.jetify.com/pkg/auth" ) type pullCmdFlags struct { @@ -32,10 +27,10 @@ func pullCmd() *cobra.Command { Use: "pull | ", Short: "Pull a config from a file or URL", Long: "Pull a config from a file or URL. URLs must be prefixed with 'http://' or 'https://'.", - Args: cobra.MaximumNArgs(1), + Args: cobra.ExactArgs(1), PreRunE: ensureNixInstalled, RunE: func(cmd *cobra.Command, args []string) error { - return pullCmdFunc(cmd, goutil.GetDefaulted(args, 0), &flags) + return pullCmdFunc(cmd, args[0], &flags) }, } @@ -64,22 +59,9 @@ func pullCmdFunc(cmd *cobra.Command, url string, flags *pullCmdFlags) error { return errors.WithStack(err) } - var creds devopt.Credentials - t, err := identity.GenSession(cmd.Context()) - if err != nil && !errors.Is(err, auth.ErrNotLoggedIn) { - return errors.WithStack(err) - } else if t != nil && err == nil { - creds = devopt.Credentials{ - IDToken: t.IDToken, - Email: t.IDClaims().Email, - Sub: t.IDClaims().Subject, - } - } - err = box.Pull(cmd.Context(), devopt.PullboxOpts{ - URL: pullPath, - Overwrite: flags.force, - Credentials: creds, + URL: pullPath, + Overwrite: flags.force, }) if prompt := pullErrorPrompt(err); prompt != "" { prompt := &survey.Confirm{Message: prompt} @@ -90,16 +72,10 @@ func pullCmdFunc(cmd *cobra.Command, url string, flags *pullCmdFlags) error { return nil } err = box.Pull(cmd.Context(), devopt.PullboxOpts{ - URL: pullPath, - Overwrite: flags.force, - Credentials: creds, + URL: pullPath, + Overwrite: flags.force, }) } - if errors.Is(err, s3.ErrProfileNotFound) { - return usererr.New( - "Profile not found. Use `devbox global push` to create a new profile.", - ) - } if err != nil { return err } diff --git a/internal/boxcli/push.go b/internal/boxcli/push.go index e0d2664a205..7251edde7ea 100644 --- a/internal/boxcli/push.go +++ b/internal/boxcli/push.go @@ -6,12 +6,9 @@ package boxcli import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "go.jetify.com/pkg/auth" "go.jetify.com/devbox/internal/devbox" "go.jetify.com/devbox/internal/devbox/devopt" - "go.jetify.com/devbox/internal/devbox/providers/identity" - "go.jetify.com/devbox/internal/goutil" ) type pushCmdFlags struct { @@ -21,12 +18,11 @@ type pushCmdFlags struct { func pushCmd() *cobra.Command { flags := pushCmdFlags{} cmd := &cobra.Command{ - Use: "push ", - Short: "Push a [global] config. Leave empty to use jetify cloud. Can " + - "be a git repo for self storage.", - Args: cobra.MaximumNArgs(1), + Use: "push ", + Short: "Push a [global] config to a git repo", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return pushCmdFunc(cmd, goutil.GetDefaulted(args, 0), flags) + return pushCmdFunc(cmd, args[0], flags) }, } @@ -44,19 +40,5 @@ func pushCmdFunc(cmd *cobra.Command, url string, flags pushCmdFlags) error { if err != nil { return errors.WithStack(err) } - t, err := identity.GenSession(cmd.Context()) - var creds devopt.Credentials - if err != nil && !errors.Is(err, auth.ErrNotLoggedIn) { - return errors.WithStack(err) - } else if t != nil && err == nil { - creds = devopt.Credentials{ - IDToken: t.IDToken, - Email: t.IDClaims().Email, - Sub: t.IDClaims().Subject, - } - } - return box.Push(cmd.Context(), devopt.PullboxOpts{ - URL: url, - Credentials: creds, - }) + return box.Push(cmd.Context(), devopt.PullboxOpts{URL: url}) } diff --git a/internal/devbox/devopt/devboxopts.go b/internal/devbox/devopt/devboxopts.go index 9afeaccc4c1..cd4d5949450 100644 --- a/internal/devbox/devopt/devboxopts.go +++ b/internal/devbox/devopt/devboxopts.go @@ -42,16 +42,8 @@ type EnvrcOpts struct { } type PullboxOpts struct { - Overwrite bool - URL string - Credentials Credentials -} - -type Credentials struct { - IDToken string - // TODO We can just parse these out, but don't want to add a dependency right now - Email string - Sub string + Overwrite bool + URL string } type AddOpts struct { diff --git a/internal/goutil/goutil.go b/internal/goutil/goutil.go index 5548fc5596c..a309407874b 100644 --- a/internal/goutil/goutil.go +++ b/internal/goutil/goutil.go @@ -10,11 +10,3 @@ func PickByKeysSorted[K comparable, V any](in map[K]V, keys []K) []V { } return out } - -func GetDefaulted[T any](in []T, index int) T { - var t T - if index >= len(in) { - return t - } - return in[index] -} diff --git a/internal/pullbox/pullbox.go b/internal/pullbox/pullbox.go index b792ca5dcfd..a8d38081ca4 100644 --- a/internal/pullbox/pullbox.go +++ b/internal/pullbox/pullbox.go @@ -15,7 +15,6 @@ import ( "go.jetify.com/devbox/internal/boxcli/usererr" "go.jetify.com/devbox/internal/devbox/devopt" "go.jetify.com/devbox/internal/pullbox/git" - "go.jetify.com/devbox/internal/pullbox/s3" "go.jetify.com/devbox/internal/pullbox/tar" "go.jetify.com/devbox/internal/ux" ) @@ -42,6 +41,10 @@ func (p *pullbox) Pull(ctx context.Context) error { defer trace.StartRegion(ctx, "Pull").End() var err error + if p.URL == "" { + return usererr.New("Nothing to pull from. Pass a git repo, URL, or file path to pull from.") + } + notEmpty, err := profileIsNotEmpty(p.ProjectDir()) if err != nil { return err @@ -49,25 +52,10 @@ func (p *pullbox) Pull(ctx context.Context) error { return fs.ErrExist } - if p.URL != "" { - ux.Finfof(os.Stderr, "Pulling global config from %s\n", p.URL) - } else { - ux.Finfof(os.Stderr, "Pulling global config\n") - } + ux.Finfof(os.Stderr, "Pulling global config from %s\n", p.URL) var tmpDir string - if p.URL == "" { - if p.Credentials.IDToken == "" { - return usererr.New("Not logged in") - } - profile := "default" // TODO: make this editable - if tmpDir, err = s3.PullToTmp(ctx, &p.Credentials, profile); err != nil { - return err - } - return p.copyToProfile(tmpDir) - } - if git.IsRepoURL(p.URL) { if tmpDir, err = git.CloneToTmp(p.URL); err != nil { return err @@ -102,24 +90,9 @@ func (p *pullbox) Pull(ctx context.Context) error { } func (p *pullbox) Push(ctx context.Context) error { - if p.URL != "" { - ux.Finfof(os.Stderr, "Pushing global config to %s\n", p.URL) - } else { - ux.Finfof(os.Stderr, "Pushing global config\n") - } - if p.URL == "" { - profile := "default" // TODO: make this editable - if p.Credentials.IDToken == "" { - return usererr.New("Not logged in") - } - ux.Finfof( - os.Stderr, - "Logged in as %s, pushing to to devbox cloud (profile: %s)\n", - p.Credentials.Email, - profile, - ) - return s3.Push(ctx, &p.Credentials, p.ProjectDir(), profile) + return usererr.New("Nowhere to push to. Pass a git repo to push to.") } + ux.Finfof(os.Stderr, "Pushing global config to %s\n", p.URL) return git.Push(ctx, p.ProjectDir(), p.URL) } diff --git a/internal/pullbox/s3/config.go b/internal/pullbox/s3/config.go deleted file mode 100644 index ff6fdac523a..00000000000 --- a/internal/pullbox/s3/config.go +++ /dev/null @@ -1,55 +0,0 @@ -package s3 - -import ( - "context" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials" - "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/pkg/errors" - "go.jetify.com/devbox/internal/devbox/devopt" -) - -// TODO(landau): We could make these customizable so folks can use their own -// buckets and roles. Would require removing user from this lib. -const ( - roleArn = "arn:aws:iam::984256416385:role/JetpackS3Federated" - bucket = "devbox.sh" - // this is a fixed value the bucket resides in this region, otherwise, - // user's default region will get pulled from config and region mismatch - // will result in user not being able to run global push - region = "us-east-2" -) - -func assumeRole(ctx context.Context, c *devopt.Credentials) (*aws.Config, error) { - noPermsConfig, _ := config.LoadDefaultConfig(ctx) - stsClient := sts.NewFromConfig(noPermsConfig) - creds, err := stsClient.AssumeRoleWithWebIdentity( - ctx, - &sts.AssumeRoleWithWebIdentityInput{ - RoleArn: aws.String(roleArn), - RoleSessionName: aws.String(c.Email), - WebIdentityToken: aws.String(c.IDToken), - }, - ) - if err != nil { - return nil, err - } - - config, err := config.LoadDefaultConfig( - ctx, - config.WithCredentialsProvider( - credentials.NewStaticCredentialsProvider( - *creds.Credentials.AccessKeyId, - *creds.Credentials.SecretAccessKey, - *creds.Credentials.SessionToken, - ), - ), - ) - config.Region = region - if err != nil { - return nil, errors.WithStack(err) - } - return &config, err -} diff --git a/internal/pullbox/s3/pull.go b/internal/pullbox/s3/pull.go deleted file mode 100644 index c12a73d3762..00000000000 --- a/internal/pullbox/s3/pull.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package s3 - -import ( - "context" - "fmt" - "os" - "strings" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/feature/s3/manager" - "github.com/aws/aws-sdk-go-v2/service/s3" - "github.com/pkg/errors" - "go.jetify.com/devbox/internal/devbox/devopt" - "go.jetify.com/devbox/internal/pullbox/tar" - "go.jetify.com/devbox/internal/ux" -) - -var ErrProfileNotFound = errors.New("profile not found") - -func PullToTmp( - ctx context.Context, - creds *devopt.Credentials, - profile string, -) (string, error) { - config, err := assumeRole(ctx, creds) - if err != nil { - return "", err - } - - // TODO(landau), before pulling, ensure that the profile exists in the cloud - s3Client := manager.NewDownloader(s3.NewFromConfig(*config)) - buf := manager.WriteAtBuffer{} - - ux.Finfof( - os.Stderr, - "Logged in as %s, pulling from jetify cloud (profile: %s)\n", - creds.Email, - profile, - ) - - if _, err = s3Client.Download( - ctx, - &buf, - &s3.GetObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String( - fmt.Sprintf( - "profiles/%s/%s.tar.gz", - creds.Sub, - profile, - ), - ), - }, - // TODO, we can use an s3 list objects to make this more accurate - ); err != nil && strings.Contains(err.Error(), "AccessDenied") { - return "", ErrProfileNotFound - } else if err != nil { - return "", errors.WithStack(err) - } - - dir, err := tar.Extract(buf.Bytes()) - if err != nil { - return "", err - } - - ux.Fsuccessf( - os.Stderr, - "Profile successfully pulled (profile: %s)\n", - profile, - ) - - return dir, nil -} diff --git a/internal/pullbox/s3/push.go b/internal/pullbox/s3/push.go deleted file mode 100644 index 6433db85e79..00000000000 --- a/internal/pullbox/s3/push.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2024 Jetify Inc. and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package s3 - -import ( - "context" - "fmt" - "io" - "os" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/feature/s3/manager" - "github.com/aws/aws-sdk-go-v2/service/s3" - "go.jetify.com/devbox/internal/devbox/devopt" - "go.jetify.com/devbox/internal/pullbox/tar" - "go.jetify.com/devbox/internal/ux" -) - -func Push( - ctx context.Context, - creds *devopt.Credentials, - dir, profile string, -) error { - archivePath, err := tar.Compress(dir) - if err != nil { - return err - } - - config, err := assumeRole(ctx, creds) - if err != nil { - return err - } - - s3Client := manager.NewUploader(s3.NewFromConfig(*config)) - file, err := os.Open(archivePath) - if err != nil { - return err - } - defer file.Close() - - _, err = s3Client.Upload(ctx, &s3.PutObjectInput{ - Bucket: aws.String(bucket), - Key: aws.String( - fmt.Sprintf( - "profiles/%s/%s.tar.gz", - creds.Sub, - profile, - ), - ), - Body: io.Reader(file), - }) - if err != nil { - return err - } - - ux.Fsuccessf( - os.Stderr, - "Profile successfully pushed (profile: %s)\n", - profile, - ) - - return nil -}