diff --git a/cmd/command/up/up.go b/cmd/command/up/up.go index a5599c5e..d1ac9260 100644 --- a/cmd/command/up/up.go +++ b/cmd/command/up/up.go @@ -120,6 +120,7 @@ func (p *Plural) handleUp(c *cli.Context) error { } ctx, err := up.Build(c.Bool("cloud")) + ctx.IgnorePreflights(c.Bool("ignore-preflights") || c.Bool("dry-run")) if err != nil { return err } diff --git a/go.mod b/go.mod index e3e660a8..a641243d 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,7 @@ require ( github.com/posthog/posthog-go v1.4.10 github.com/samber/lo v1.52.0 github.com/urfave/cli v1.22.16 + github.com/whilp/git-urls v1.0.0 github.com/yuin/gopher-lua v1.1.1 gitlab.com/gitlab-org/api/client-go v0.128.0 golang.org/x/crypto v0.48.0 diff --git a/go.sum b/go.sum index 800d55c3..7df2ce5c 100644 --- a/go.sum +++ b/go.sum @@ -740,6 +740,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNl github.com/vmihailenco/msgpack/v4 v4.3.13/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= +github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= diff --git a/pkg/client/plural.go b/pkg/client/plural.go index 901c427e..0d11537d 100644 --- a/pkg/client/plural.go +++ b/pkg/client/plural.go @@ -111,7 +111,7 @@ func (p *Plural) HandleInitWithProject(c *cli.Context) (*manifest.ProjectManifes repo := "" p.InitPluralClient() - git, err := wkspace.Preflight(c.Bool("dry-run")) + git, err := wkspace.Preflight(c.Bool("dry-run"), c.Bool("ignore-preflights")) if err != nil && (git || c.Bool("dry-run")) { return nil, err } diff --git a/pkg/up/context.go b/pkg/up/context.go index f1f63baa..c0ee5cec 100644 --- a/pkg/up/context.go +++ b/pkg/up/context.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/AlecAivazis/survey/v2" @@ -17,19 +18,21 @@ import ( "github.com/pluralsh/plural-cli/pkg/utils/git" "github.com/mitchellh/go-homedir" + giturls "github.com/whilp/git-urls" ) type Context struct { - Provider providerapi.Provider - Manifest *manifest.ProjectManifest - Config *config.Config - Cloud bool - RepoUrl string - StacksIdentity string - Delims *delims - ImportCluster *string - CloudCluster string - dir string + Provider providerapi.Provider + Manifest *manifest.ProjectManifest + Config *config.Config + Cloud bool + RepoUrl string + StacksIdentity string + Delims *delims + ImportCluster *string + CloudCluster string + dir string + ignorePreflights bool } type delims struct { @@ -50,6 +53,10 @@ func (ctx *Context) changeDelims() { ctx.Delims = &delims{"[[", "]]"} } +func (ctx *Context) IgnorePreflights(ignore bool) { + ctx.ignorePreflights = ignore +} + func (ctx *Context) SetImportCluster(id string) { ctx.ImportCluster = lo.ToPtr(id) } @@ -57,16 +64,16 @@ func (ctx *Context) SetImportCluster(id string) { func (ctx *Context) Backfill() error { context, err := manifest.FetchContext() if err != nil { - return backfillConsoleContext(ctx.Manifest) + return ctx.backfillConsoleContext(ctx.Manifest) } console, ok := context.Configuration["console"] if !ok { - return backfillConsoleContext(ctx.Manifest) + return ctx.backfillConsoleContext(ctx.Manifest) } if _, ok = console["private_key"]; !ok { - return backfillConsoleContext(ctx.Manifest) + return ctx.backfillConsoleContext(ctx.Manifest) } if v, ok := console["repo_url"]; ok { @@ -103,7 +110,7 @@ func Build(cloud bool) (*Context, error) { }, nil } -func backfillConsoleContext(_ *manifest.ProjectManifest) error { +func (context *Context) backfillConsoleContext(_ *manifest.ProjectManifest) error { path := manifest.ContextPath() ctx, err := manifest.FetchContext() if err != nil { @@ -115,7 +122,17 @@ func backfillConsoleContext(_ *manifest.ProjectManifest) error { console = map[string]interface{}{} } - utils.Highlight("It looks like you cloned this repo before running plural up, we just need you to generate and give us a deploy key to continue\n") + utils.Highlight("It looks like you cloned this repo before running plural up, we just need to ensure authentication is setup correctly to continue\n") + + url, err := git.GetURL() + if err != nil { + return err + } + + if strings.HasPrefix(url, "http") { + return fmt.Errorf("found non-ssh upstream url %s, please reclone the repo with SSH and retry", url) + } + utils.Highlight("If you want, you can use `plural crypto ssh-keygen` to generate a keypair to use as a deploy key as well\n\n") files, err := filepath.Glob(filepath.Join(os.Getenv("HOME"), ".ssh", "*")) @@ -144,17 +161,10 @@ func backfillConsoleContext(_ *manifest.ProjectManifest) error { return err } - url, err := git.GetURL() - if err != nil { - return err - } - - if strings.HasPrefix(url, "http") { - return fmt.Errorf("found non-ssh upstream url %s, please reclone the repo with SSH and retry", url) - } - - if err := verifySSHKey(contents, url); err != nil { - return fmt.Errorf("ssh key not valid for url %s, error: %w", url, err) + if !context.ignorePreflights { + if err := verifySSHKey(contents, url); err != nil { + return fmt.Errorf("ssh key not valid for url %s, error: %w. If you want to bypass this check, you can use the --ignore-preflights flag", url, err) + } } console["repo_url"] = url @@ -174,9 +184,30 @@ func verifySSHKey(key, url string) error { return } }(dir) - auth, _ := git.SSHAuth("git", key, "") + + auth, _ := git.SSHAuth(getGitUsername(url), key, "") if _, err := git.Clone(auth, url, dir); err != nil { return err } return nil } + +var ( + scpSyntax = regexp.MustCompile(`^([a-zA-Z0-9-._~]+@)?([a-zA-Z0-9._-]+):([a-zA-Z0-9./._-]+)(?:\?||$)(.*)$`) +) + +func getGitUsername(url string) string { + match := scpSyntax.FindAllStringSubmatch(url, -1) + if len(match) > 0 { + if match[0][1] != "" { + return strings.TrimRight(match[0][1], "@") + } + } + + uname := "git" + parsedUrl, err := giturls.Parse(url) + if err == nil { + uname = parsedUrl.User.Username() + } + return uname +} diff --git a/pkg/up/context_test.go b/pkg/up/context_test.go new file mode 100644 index 00000000..fc1f6818 --- /dev/null +++ b/pkg/up/context_test.go @@ -0,0 +1,44 @@ +package up + +import "testing" + +func TestGetGitUsername(t *testing.T) { + tests := []struct { + name string + url string + want string + }{ + { + name: "github scp-style", + url: "git@github.com:acme-corp/widget-service.git", + want: "git", + }, + { + name: "github ssh transport", + url: "ssh://git@github.com/acme-corp/widget-service.git", + want: "git", + }, + { + name: "gitlab nested group", + url: "git@gitlab.com:engineering/platform/api-gateway.git", + want: "git", + }, + { + name: "azure devops ssh (v3 path)", + url: "my-org@ssh.dev.azure.com:v3/MyOrg/MyProject/MyRepo", + want: "my-org", + }, + { + name: "bitbucket cloud workspace repo", + url: "another-org@bitbucket.org:acme-workspace/mobile-app.git", + want: "another-org", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getGitUsername(tt.url); got != tt.want { + t.Errorf("getGitUsername(%q) = %q, want %q", tt.url, got, tt.want) + } + }) + } +} diff --git a/pkg/wkspace/validator.go b/pkg/wkspace/validator.go index 40de6392..567c7666 100644 --- a/pkg/wkspace/validator.go +++ b/pkg/wkspace/validator.go @@ -12,7 +12,7 @@ import ( "github.com/pluralsh/plural-cli/pkg/utils/git" ) -func Preflight(dryRun bool) (bool, error) { +func Preflight(dryRun, ignorePreflights bool) (bool, error) { requirements := []string{"terraform", "git"} if dryRun { requirements = []string{"git"} @@ -24,7 +24,7 @@ func Preflight(dryRun bool) (bool, error) { } } - if !dryRun { + if !dryRun && !ignorePreflights { fmt.Print("\nTesting if git ssh is properly configured...") if err := checkGitSSH(); err != nil { fmt.Printf("%s\n\n", err.Error())