Skip to content

Commit 67aa668

Browse files
Improve authentication flow when migrating from heroku (#1371)
* pull dry run into new package * automatically authenticate with heroku * move login and agree_tos into login package * factor out RequireLoginAndToS * move prettyError into client * push up interactivity check * break out cluster package * pas fabric and cluster addr into InteractiveRequireLoginAndToS * mv InteractiveRequireLoginAndToS into login package * use login.InteractiveRequireLoginAndToS in MigrateFromHeroku
1 parent a1cab63 commit 67aa668

32 files changed

+351
-218
lines changed

src/cmd/cli/command/commands.go

Lines changed: 20 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import (
2020
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc/gcp"
2121
"github.com/DefangLabs/defang/src/pkg/cli/compose"
2222
"github.com/DefangLabs/defang/src/pkg/clouds/aws"
23+
pcluster "github.com/DefangLabs/defang/src/pkg/cluster"
24+
"github.com/DefangLabs/defang/src/pkg/dryrun"
25+
"github.com/DefangLabs/defang/src/pkg/login"
2326
"github.com/DefangLabs/defang/src/pkg/logs"
2427
"github.com/DefangLabs/defang/src/pkg/mcp"
2528
"github.com/DefangLabs/defang/src/pkg/migrate"
@@ -64,19 +67,6 @@ func getCluster() string {
6467
return org + "@" + cluster
6568
}
6669

67-
func prettyError(err error) error {
68-
// To avoid printing the internal gRPC error code
69-
var cerr *connect.Error
70-
if errors.As(err, &cerr) {
71-
term.Debug("Server error:", cerr)
72-
err = errors.Unwrap(cerr)
73-
}
74-
if cli.IsNetworkError(err) {
75-
return fmt.Errorf("%w; please check network settings and try again", err)
76-
}
77-
return err
78-
}
79-
8070
func Execute(ctx context.Context) error {
8171
if term.StdoutCanColor() {
8272
restore := term.EnableANSI()
@@ -85,10 +75,10 @@ func Execute(ctx context.Context) error {
8575

8676
if err := RootCmd.ExecuteContext(ctx); err != nil {
8777
if !(errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
88-
term.Error("Error:", prettyError(err))
78+
term.Error("Error:", cliClient.PrettyError(err))
8979
}
9080

91-
if err == cli.ErrDryRun {
81+
if err == dryrun.ErrDryRun {
9282
return nil
9383
}
9484

@@ -169,14 +159,14 @@ func SetupCommands(ctx context.Context, version string) {
169159

170160
RootCmd.Version = version
171161
RootCmd.PersistentFlags().Var(&colorMode, "color", fmt.Sprintf(`colorize output; one of %v`, allColorModes))
172-
RootCmd.PersistentFlags().StringVarP(&cluster, "cluster", "s", cli.DefangFabric, "Defang cluster to connect to")
162+
RootCmd.PersistentFlags().StringVarP(&cluster, "cluster", "s", pcluster.DefangFabric, "Defang cluster to connect to")
173163
RootCmd.PersistentFlags().MarkHidden("cluster")
174164
RootCmd.PersistentFlags().StringVar(&org, "org", os.Getenv("DEFANG_ORG"), "override GitHub organization name (tenant)")
175165
RootCmd.PersistentFlags().VarP(&providerID, "provider", "P", fmt.Sprintf(`bring-your-own-cloud provider; one of %v`, cliClient.AllProviders()))
176166
// RootCmd.Flag("provider").NoOptDefVal = "auto" NO this will break the "--provider aws"
177167
RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging") // backwards compat: only used by tail
178168
RootCmd.PersistentFlags().BoolVar(&doDebug, "debug", pkg.GetenvBool("DEFANG_DEBUG"), "debug logging for troubleshooting the CLI")
179-
RootCmd.PersistentFlags().BoolVar(&cli.DoDryRun, "dry-run", false, "dry run (don't actually change anything)")
169+
RootCmd.PersistentFlags().BoolVar(&dryrun.DoDryRun, "dry-run", false, "dry run (don't actually change anything)")
180170
RootCmd.PersistentFlags().BoolVarP(&nonInteractive, "non-interactive", "T", !hasTty, "disable interactive prompts / no TTY")
181171
RootCmd.PersistentFlags().StringP("project-name", "p", "", "project name")
182172
RootCmd.PersistentFlags().StringP("cwd", "C", "", "change directory before running the command")
@@ -346,6 +336,7 @@ var RootCmd = &cobra.Command{
346336
Args: cobra.NoArgs,
347337
Short: "Defang CLI is used to take your app from Docker Compose to a secure and scalable deployment on your favorite cloud in minutes.",
348338
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
339+
ctx := cmd.Context()
349340
term.SetDebug(doDebug)
350341

351342
// Don't track/connect the completion commands
@@ -373,9 +364,9 @@ var RootCmd = &cobra.Command{
373364
}
374365
}
375366

376-
client, err = cli.Connect(cmd.Context(), getCluster())
367+
client, err = cli.Connect(ctx, getCluster())
377368

378-
if v, err := client.GetVersions(cmd.Context()); err == nil {
369+
if v, err := client.GetVersions(ctx); err == nil {
379370
version := cmd.Root().Version // HACK to avoid circular dependency with RootCmd
380371
term.Debug("Fabric:", v.Fabric, "CLI:", version, "CLI-Min:", v.CliMin)
381372
if hasTty && isNewer(version, v.CliMin) && !isUpgradeCommand(cmd) {
@@ -389,40 +380,10 @@ var RootCmd = &cobra.Command{
389380
return nil
390381
}
391382

392-
if err = client.CheckLoginAndToS(cmd.Context()); err != nil {
393-
if nonInteractive {
394-
return err
395-
}
396-
// Login interactively now; only do this for authorization-related errors
397-
if connect.CodeOf(err) == connect.CodeUnauthenticated {
398-
term.Debug("Server error:", err)
399-
term.Warn("Please log in to continue.")
400-
term.ResetWarnings() // clear any previous warnings so we don't show them again
401-
402-
defer func() { track.Cmd(nil, "Login", P("reason", err)) }()
403-
if err = cli.InteractiveLogin(cmd.Context(), client, getCluster()); err != nil {
404-
return err
405-
}
406-
407-
// Reconnect with the new token
408-
if client, err = cli.Connect(cmd.Context(), getCluster()); err != nil {
409-
return err
410-
}
411-
412-
if err = client.CheckLoginAndToS(cmd.Context()); err == nil { // recheck (new token = new user)
413-
return nil // success
414-
}
415-
}
416-
417-
// Check if the user has agreed to the terms of service and show a prompt if needed
418-
if connect.CodeOf(err) == connect.CodeFailedPrecondition {
419-
term.Warn(prettyError(err))
420-
421-
defer func() { track.Cmd(nil, "Terms", P("reason", err)) }()
422-
if err = cli.InteractiveAgreeToS(cmd.Context(), client); err != nil {
423-
return err // fatal
424-
}
425-
}
383+
if nonInteractive {
384+
err = client.CheckLoginAndToS(ctx)
385+
} else {
386+
err = login.InteractiveRequireLoginAndToS(ctx, client, getCluster())
426387
}
427388

428389
return err
@@ -437,11 +398,11 @@ var loginCmd = &cobra.Command{
437398
trainingOptOut, _ := cmd.Flags().GetBool("training-opt-out")
438399

439400
if nonInteractive {
440-
if err := cli.NonInteractiveGitHubLogin(cmd.Context(), client, getCluster()); err != nil {
401+
if err := login.NonInteractiveGitHubLogin(cmd.Context(), client, getCluster()); err != nil {
441402
return err
442403
}
443404
} else {
444-
err := cli.InteractiveLogin(cmd.Context(), client, getCluster())
405+
err := login.InteractiveLogin(cmd.Context(), client, getCluster())
445406
if err != nil {
446407
return err
447408
}
@@ -755,7 +716,7 @@ var configDeleteCmd = &cobra.Command{
755716
if err := cli.ConfigDelete(cmd.Context(), projectName, provider, names...); err != nil {
756717
// Show a warning (not an error) if the config was not found
757718
if connect.CodeOf(err) == connect.CodeNotFound {
758-
term.Warn(prettyError(err))
719+
term.Warn(cliClient.PrettyError(err))
759720
return nil
760721
}
761722
return err
@@ -870,7 +831,7 @@ var deleteCmd = &cobra.Command{
870831
if err != nil {
871832
if connect.CodeOf(err) == connect.CodeNotFound {
872833
// Show a warning (not an error) if the service was not found
873-
term.Warn(prettyError(err))
834+
term.Warn(cliClient.PrettyError(err))
874835
return nil
875836
}
876837
return err
@@ -998,15 +959,15 @@ var tosCmd = &cobra.Command{
998959
agree, _ := cmd.Flags().GetBool("agree-tos")
999960

1000961
if agree {
1001-
return cli.NonInteractiveAgreeToS(cmd.Context(), client)
962+
return login.NonInteractiveAgreeToS(cmd.Context(), client)
1002963
}
1003964

1004965
if nonInteractive {
1005966
printDefangHint("To agree to the terms of service, do:", cmd.CalledAs()+" --agree-tos")
1006967
return nil
1007968
}
1008969

1009-
return cli.InteractiveAgreeToS(cmd.Context(), client)
970+
return login.InteractiveAgreeToS(cmd.Context(), client)
1010971
},
1011972
}
1012973

src/cmd/cli/command/compose.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
1818
"github.com/DefangLabs/defang/src/pkg/cli/client/byoc"
1919
"github.com/DefangLabs/defang/src/pkg/cli/compose"
20+
"github.com/DefangLabs/defang/src/pkg/dryrun"
2021
"github.com/DefangLabs/defang/src/pkg/logs"
2122
"github.com/DefangLabs/defang/src/pkg/term"
2223
"github.com/DefangLabs/defang/src/pkg/track"
@@ -229,7 +230,7 @@ func handleComposeUpErr(ctx context.Context, err error, project *compose.Project
229230

230231
if strings.Contains(err.Error(), "maximum number of projects") {
231232
if projectName, err2 := provider.RemoteProjectName(ctx); err2 == nil {
232-
term.Error("Error:", prettyError(err))
233+
term.Error("Error:", cliClient.PrettyError(err))
233234
if _, err := cli.InteractiveComposeDown(ctx, provider, projectName); err != nil {
234235
term.Debug("ComposeDown failed:", err)
235236
printDefangHint("To deactivate a project, do:", "compose down --project-name "+projectName)
@@ -242,7 +243,7 @@ func handleComposeUpErr(ctx context.Context, err error, project *compose.Project
242243
return err
243244
}
244245

245-
term.Error("Error:", prettyError(err))
246+
term.Error("Error:", cliClient.PrettyError(err))
246247
track.Evt("Debug Prompted", P("composeErr", err))
247248
return cli.InteractiveDebugForClientError(ctx, client, project, err)
248249
}
@@ -375,7 +376,7 @@ func makeComposeDownCmd() *cobra.Command {
375376
if err != nil {
376377
if connect.CodeOf(err) == connect.CodeNotFound {
377378
// Show a warning (not an error) if the service was not found
378-
term.Warn(prettyError(err))
379+
term.Warn(cliClient.PrettyError(err))
379380
return nil
380381
}
381382
return err
@@ -464,7 +465,7 @@ func makeComposeConfigCmd() *cobra.Command {
464465
}
465466

466467
_, _, err = cli.ComposeUp(ctx, project, client, provider, compose.UploadModeIgnore, defangv1.DeploymentMode_MODE_UNSPECIFIED)
467-
if !errors.Is(err, cli.ErrDryRun) {
468+
if !errors.Is(err, dryrun.ErrDryRun) {
468469
return err
469470
}
470471
return nil

src/cmd/cli/command/deploymentinfo.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import (
44
"regexp"
55
"strings"
66

7-
"github.com/DefangLabs/defang/src/pkg/cli"
87
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
8+
pcluster "github.com/DefangLabs/defang/src/pkg/cluster"
99
"github.com/DefangLabs/defang/src/pkg/term"
1010
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
1111
)
@@ -15,7 +15,7 @@ const SERVICE_PORTAL_URL = "https://" + DEFANG_PORTAL_HOST + "/service"
1515

1616
func printPlaygroundPortalServiceURLs(serviceInfos []*defangv1.ServiceInfo) {
1717
// We can only show services deployed to the prod1 defang SaaS environment.
18-
if providerID == cliClient.ProviderDefang && cluster == cli.DefaultCluster {
18+
if providerID == cliClient.ProviderDefang && cluster == pcluster.DefaultCluster {
1919
term.Info("Monitor your services' status in the defang portal")
2020
for _, serviceInfo := range serviceInfos {
2121
term.Println(" -", SERVICE_PORTAL_URL+"/"+serviceInfo.Service.Name)

src/cmd/cli/command/deploymentinfo_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import (
66
"strings"
77
"testing"
88

9-
"github.com/DefangLabs/defang/src/pkg/cli"
109
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
10+
pcluster "github.com/DefangLabs/defang/src/pkg/cluster"
1111
"github.com/DefangLabs/defang/src/pkg/term"
1212
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
1313
)
@@ -22,7 +22,7 @@ func TestPrintPlaygroundPortalServiceURLs(t *testing.T) {
2222
term.DefaultTerm = term.NewTerm(os.Stdin, &stdout, &stderr)
2323

2424
providerID = cliClient.ProviderDefang
25-
cluster = cli.DefaultCluster
25+
cluster = pcluster.DefaultCluster
2626
printPlaygroundPortalServiceURLs([]*defangv1.ServiceInfo{
2727
{
2828
Service: &defangv1.Service{Name: "service1"},

src/cmd/cli/command/mcp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"os"
66
"path/filepath"
77

8-
"github.com/DefangLabs/defang/src/pkg/cli"
98
cliClient "github.com/DefangLabs/defang/src/pkg/cli/client"
9+
"github.com/DefangLabs/defang/src/pkg/login"
1010
"github.com/DefangLabs/defang/src/pkg/mcp"
1111
"github.com/DefangLabs/defang/src/pkg/mcp/resources"
1212
"github.com/DefangLabs/defang/src/pkg/mcp/tools"
@@ -84,7 +84,7 @@ set_config - This tool sets or updates configuration variables for a deployed ap
8484
term.Debug("Function invoked: cli.InteractiveLoginInsideDocker")
8585

8686
go func() {
87-
if err := cli.InteractiveLoginInsideDocker(cmd.Context(), getCluster(), authPort); err != nil {
87+
if err := login.InteractiveLoginInsideDocker(cmd.Context(), getCluster(), authPort); err != nil {
8888
term.Error("Failed to start auth server", "error", err)
8989
}
9090
}()

src/pkg/cli/bootstrap.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/DefangLabs/defang/src/pkg"
1111
"github.com/DefangLabs/defang/src/pkg/cli/client"
12+
"github.com/DefangLabs/defang/src/pkg/dryrun"
1213
"github.com/DefangLabs/defang/src/pkg/logs"
1314
"github.com/DefangLabs/defang/src/pkg/term"
1415
)
@@ -19,8 +20,8 @@ func BootstrapCommand(ctx context.Context, projectName string, verbose bool, pro
1920
} else {
2021
term.Infof("Running CD command %q in project %q", cmd, projectName)
2122
}
22-
if DoDryRun {
23-
return ErrDryRun
23+
if dryrun.DoDryRun {
24+
return dryrun.ErrDryRun
2425
}
2526

2627
since := time.Now()
@@ -67,8 +68,8 @@ func SplitProjectStack(name string) (projectName string, stackName string) {
6768

6869
func BootstrapLocalList(ctx context.Context, provider client.Provider) error {
6970
term.Debug("Running CD list")
70-
if DoDryRun {
71-
return ErrDryRun
71+
if dryrun.DoDryRun {
72+
return dryrun.ErrDryRun
7273
}
7374

7475
stacks, err := provider.BootstrapList(ctx)

src/pkg/cli/client/pretty_error.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package client
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/DefangLabs/defang/src/pkg/term"
9+
"github.com/bufbuild/connect-go"
10+
)
11+
12+
func PrettyError(err error) error {
13+
// To avoid printing the internal gRPC error code
14+
var cerr *connect.Error
15+
if errors.As(err, &cerr) {
16+
term.Debug("Server error:", cerr)
17+
err = errors.Unwrap(cerr)
18+
}
19+
if IsNetworkError(err) {
20+
return fmt.Errorf("%w; please check network settings and try again", err)
21+
}
22+
return err
23+
}
24+
25+
func IsNetworkError(err error) bool {
26+
if err == nil {
27+
return false
28+
}
29+
errStr := err.Error()
30+
lastColon := strings.LastIndexByte(errStr, ':')
31+
switch errStr[lastColon+1:] { // +1 to skip the colon and handle the case where there is no colon
32+
case " connection refused",
33+
" i/o timeout",
34+
" network is unreachable",
35+
" no such host",
36+
" unexpected EOF",
37+
" device or resource busy":
38+
return true
39+
}
40+
return false
41+
}

src/pkg/cli/common.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,13 @@ package cli
22

33
import (
44
"encoding/json"
5-
"errors"
65

76
"github.com/DefangLabs/defang/src/pkg/term"
87
"google.golang.org/protobuf/encoding/protojson"
98
"google.golang.org/protobuf/proto"
109
"gopkg.in/yaml.v3"
1110
)
1211

13-
var (
14-
DoDryRun = false
15-
16-
ErrDryRun = errors.New("dry run")
17-
)
18-
1912
func MarshalPretty(root string, data proto.Message) ([]byte, error) {
2013
// HACK: convert to JSON first so we respect the json tags (like "omitempty")
2114
bytes, err := protojson.Marshal(data)

src/pkg/cli/composeDown.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/AlecAivazis/survey/v2"
99
"github.com/DefangLabs/defang/src/pkg/cli/client"
10+
"github.com/DefangLabs/defang/src/pkg/dryrun"
1011
"github.com/DefangLabs/defang/src/pkg/term"
1112
"github.com/DefangLabs/defang/src/pkg/track"
1213
"github.com/DefangLabs/defang/src/pkg/types"
@@ -17,8 +18,8 @@ import (
1718
func ComposeDown(ctx context.Context, projectName string, c client.FabricClient, provider client.Provider, names ...string) (types.ETag, error) {
1819
term.Debugf("Destroying project %q %q", projectName, names)
1920

20-
if DoDryRun {
21-
return "", ErrDryRun
21+
if dryrun.DoDryRun {
22+
return "", dryrun.ErrDryRun
2223
}
2324

2425
if len(names) == 0 {

0 commit comments

Comments
 (0)