diff --git a/admin/server/github.go b/admin/server/github.go index e4adb0e518b..c6539f0327d 100644 --- a/admin/server/github.go +++ b/admin/server/github.go @@ -231,7 +231,7 @@ func (s *Server) registerGithubEndpoints(mux *http.ServeMux) { observability.MuxHandle(inner, "/github/connect/callback", s.authenticator.HTTPMiddleware(middleware.Check(s.checkGithubRateLimit("/github/connect/callback"), http.HandlerFunc(s.githubConnectCallback)))) observability.MuxHandle(inner, "/github/auth/login", s.authenticator.HTTPMiddleware(middleware.Check(s.checkGithubRateLimit("github/auth/login"), http.HandlerFunc(s.githubAuthLogin)))) observability.MuxHandle(inner, "/github/auth/callback", s.authenticator.HTTPMiddleware(middleware.Check(s.checkGithubRateLimit("github/auth/callback"), http.HandlerFunc(s.githubAuthCallback)))) - observability.MuxHandle(inner, "/github/post-auth-redirect", s.authenticator.HTTPMiddleware(middleware.Check(s.checkGithubRateLimit("github/post-auth-redirect"), http.HandlerFunc(s.githubRepoStatus)))) + observability.MuxHandle(inner, "/github/post-auth-redirect", s.authenticator.HTTPMiddleware(middleware.Check(s.checkGithubRateLimit("github/post-auth-redirect"), http.HandlerFunc(s.githubStatus)))) mux.Handle("/github/", observability.Middleware("admin", s.logger, inner)) } @@ -597,9 +597,10 @@ func (s *Server) githubWebhook(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// githubRepoStatus is a http wrapper over [GetGithubRepoStatus]. It redirects to the grantAccessURL if there is no access. +// githubStatus is a http wrapper over [GetGithubRepoStatus]/[GetGithubUserStatus] depending upon whether `remote` query is passed. +// It redirects to the grantAccessURL if there is no access. // It's implemented as a non-gRPC endpoint mounted directly on /github/post-auth-redirect. -func (s *Server) githubRepoStatus(w http.ResponseWriter, r *http.Request) { +func (s *Server) githubStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Check the request is made by an authenticated user claims := auth.GetClaims(ctx) @@ -608,18 +609,36 @@ func (s *Server) githubRepoStatus(w http.ResponseWriter, r *http.Request) { return } - resp, err := s.GetGithubRepoStatus(ctx, &adminv1.GetGithubRepoStatusRequest{GithubUrl: r.URL.Query().Get("remote")}) - if err != nil { - http.Error(w, fmt.Sprintf("failed to fetch github repo status: %s", err), http.StatusInternalServerError) - return + var ( + hasAccess bool + grantAccessURL string + remote = r.URL.Query().Get("remote") + ) + + if remote == "" { + resp, err := s.GetGithubUserStatus(ctx, &adminv1.GetGithubUserStatusRequest{}) + if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch user status: %s", err), http.StatusInternalServerError) + return + } + hasAccess = resp.HasAccess + grantAccessURL = resp.GrantAccessUrl + } else { + resp, err := s.GetGithubRepoStatus(ctx, &adminv1.GetGithubRepoStatusRequest{GithubUrl: remote}) + if err != nil { + http.Error(w, fmt.Sprintf("failed to fetch github repo status: %s", err), http.StatusInternalServerError) + return + } + hasAccess = resp.HasAccess + grantAccessURL = resp.GrantAccessUrl } - if resp.HasAccess { + if hasAccess { http.Redirect(w, r, s.urls.githubConnectSuccess, http.StatusTemporaryRedirect) return } - redirectURL, err := urlutil.WithQuery(s.urls.githubConnectUI, map[string]string{"redirect": resp.GrantAccessUrl}) + redirectURL, err := urlutil.WithQuery(s.urls.githubConnectUI, map[string]string{"redirect": grantAccessURL}) if err != nil { http.Error(w, fmt.Sprintf("failed to create redirect URL: %s", err), http.StatusInternalServerError) return diff --git a/cli/cmd/deploy/deploy.go b/cli/cmd/deploy/deploy.go index 6b8f2c81105..81721fcfbf5 100644 --- a/cli/cmd/deploy/deploy.go +++ b/cli/cmd/deploy/deploy.go @@ -158,35 +158,19 @@ func DeployFlow(ctx context.Context, ch *cmdutil.Helper, opts *Options) error { remote, githubURL, err = gitutil.ExtractGitRemote(localGitPath, opts.RemoteName, false) if err != nil { // It's not a valid remote for Github. We still navigate user to login and then ask user to chhose either to create repo manually or let rill create one for them. + silent := false if !ch.IsAuthenticated() { - err := loginWithTelemetry(ctx, ch, "") + err := loginWithTelemetryAndGithubRedirect(ctx, ch, "") if err != nil { - ch.PrintfWarn("Login failed with error: %s\n", err.Error()) + return fmt.Errorf("Login failed with error: %s\n", err.Error()) } - fmt.Println() + silent = true } if !errors.Is(err, gitutil.ErrGitRemoteNotFound) && !errors.Is(err, git.ErrRepositoryNotExists) { return err } - ch.PrintfBold(`No git remote was found. - -Rill projects deploy continuously when you push changes to Github. -Therefore, your project must be on Github before you deploy it to Rill. -`) - - printer.ColorYellowBold.Print(` -You can continue here and Rill can create a Github Repository for you or -you can exit the command and create a repository manually. - -`) - if !cmdutil.ConfirmPrompt("Do you want to continue?", "", true) { - ch.PrintfBold(githubSetupMsg) - return nil - } - // ideally this should be clubbed with login similar to main github flow to reduce switch between CLI and browser - // but it is not possible right now since we need an input from user for the org where they want to create the repo. - if err := createGithubRepoFlow(ctx, ch, localGitPath); err != nil { + if err := createGithubRepoFlow(ctx, ch, localGitPath, silent); err != nil { return err } // In the rest of the flow we still check for the github access. @@ -219,15 +203,7 @@ you can exit the command and create a repository manually. silentGitFlow := false if !ch.IsAuthenticated() { silentGitFlow = true - authURL := ch.AdminURL - if strings.Contains(authURL, "http://localhost:9090") { - authURL = "http://localhost:8080" - } - redirectURL, err := urlutil.WithQuery(urlutil.MustJoinURL(authURL, "/github/post-auth-redirect"), map[string]string{"remote": githubURL}) - if err != nil { - return err - } - if err := loginWithTelemetry(ctx, ch, redirectURL); err != nil { + if err := loginWithTelemetryAndGithubRedirect(ctx, ch, githubURL); err != nil { return err } } @@ -364,6 +340,23 @@ you can exit the command and create a repository manually. return nil } +func loginWithTelemetryAndGithubRedirect(ctx context.Context, ch *cmdutil.Helper, remote string) error { + authURL := ch.AdminURL + if strings.Contains(authURL, "http://localhost:9090") { + authURL = "http://localhost:8080" + } + var qry map[string]string + if remote != "" { + qry = map[string]string{"remote": remote} + } + + redirectURL, err := urlutil.WithQuery(urlutil.MustJoinURL(authURL, "/github/post-auth-redirect"), qry) + if err != nil { + return err + } + return loginWithTelemetry(ctx, ch, redirectURL) +} + func loginWithTelemetry(ctx context.Context, ch *cmdutil.Helper, redirectURL string) error { ch.PrintfBold("Please log in or sign up for Rill. Opening browser...\n") time.Sleep(2 * time.Second) @@ -388,7 +381,7 @@ func loginWithTelemetry(ctx context.Context, ch *cmdutil.Helper, redirectURL str return nil } -func createGithubRepoFlow(ctx context.Context, ch *cmdutil.Helper, localGitPath string) error { +func createGithubRepoFlow(ctx context.Context, ch *cmdutil.Helper, localGitPath string, silent bool) error { // Get the admin client c, err := ch.Client() if err != nil { @@ -410,7 +403,9 @@ func createGithubRepoFlow(ctx context.Context, ch *cmdutil.Helper, localGitPath ch.Print("\t" + res.GrantAccessUrl + "\n\n") // Open browser if possible - _ = browser.Open(res.GrantAccessUrl) + if !silent { + _ = browser.Open(res.GrantAccessUrl) + } } } @@ -438,12 +433,28 @@ func createGithubRepoFlow(ctx context.Context, ch *cmdutil.Helper, localGitPath // Emit success telemetry ch.Telemetry(ctx).RecordBehavioralLegacy(activity.BehavioralEventGithubConnectedSuccess) + ch.PrintfBold(`No git remote was found. + +Rill projects deploy continuously when you push changes to Github. +Therefore, your project must be on Github before you deploy it to Rill. + `) + + ch.Print(` +You can continue here and Rill can create a Github Repository for you or +you can exit the command and create a repository manually. + + `) + if !cmdutil.ConfirmPrompt("Do you want to continue?", "", true) { + ch.PrintfBold(githubSetupMsg) + return nil + } + repoOwner := pollRes.Account if len(pollRes.Organizations) > 0 { repoOwners := []string{pollRes.Account} repoOwners = append(repoOwners, pollRes.Organizations...) ch.Print("\nYou also have access to organization(s)\n\n") - repoOwner = cmdutil.SelectPrompt("Please chhose where to create the repository", repoOwners, pollRes.Account) + repoOwner = cmdutil.SelectPrompt("Select Github account", repoOwners, pollRes.Account) } // create and verify githubRepository, err := createGithubRepository(ctx, ch, pollRes, localGitPath, repoOwner) @@ -451,6 +462,8 @@ func createGithubRepoFlow(ctx context.Context, ch *cmdutil.Helper, localGitPath return err } + printer.ColorGreenBold.Printf("\nRepository %q created successfully\n\n", *githubRepository.Name) + ch.Print("Pushing local project to Github\n\n") // init git repo repo, err := git.PlainInit(localGitPath, false) if err != nil { @@ -492,7 +505,7 @@ func createGithubRepoFlow(ctx context.Context, ch *cmdutil.Helper, localGitPath return fmt.Errorf("failed to push to remote %q : %w", *githubRepository.HTMLURL, err) } - printer.ColorGreenBold.Printf("\nRepository %q created successfully. Local changes pushed to remote.\n\n", *githubRepository.Name) + ch.Print("Local changes pushed to remote\n\n") return nil } } @@ -508,12 +521,11 @@ func createGithubRepository(ctx context.Context, ch *cmdutil.Helper, pollRes *ad var githubRepo *github.Repository var err error - for i, tempRepoName := 1, repoName; i <= 10; i++ { - githubRepo, _, err = githubClient.Repositories.Create(ctx, repoOwner, &github.Repository{Name: &tempRepoName, DefaultBranch: &defaultBranch}) + for i := 1; i <= 10; i++ { + githubRepo, _, err = githubClient.Repositories.Create(ctx, repoOwner, &github.Repository{Name: &repoName, DefaultBranch: &defaultBranch}) if err == nil { break } - if strings.Contains(err.Error(), "authentication") || strings.Contains(err.Error(), "credentials") { // The users who installed app before we started including repo:write permissions need to accept permissions // and then only we can create repositories. @@ -523,8 +535,12 @@ func createGithubRepository(ctx context.Context, ch *cmdutil.Helper, pollRes *ad if !strings.Contains(err.Error(), "name already exists") { return nil, fmt.Errorf("failed to create repository: %w", err) } - // there is a name conflict - tempRepoName = repoName + fmt.Sprintf("_v%v", i) + + ch.Printf("Repository name %q is already taken\n", repoName) + repoName = cmdutil.InputPrompt("Please provide alternate name", "") + } + if err != nil { + return nil, fmt.Errorf("failed to create repository: %w", err) } // the create repo API does not wait for repo creation to be fully processed on server. Need to verify by making a get call in a loop @@ -539,7 +555,7 @@ func createGithubRepository(ctx context.Context, ch *cmdutil.Helper, pollRes *ad select { case <-pollCtx.Done(): return nil, pollCtx.Err() - case <-time.After(5 * time.Second): + case <-time.After(2 * time.Second): // Ready to check again. } _, _, err := githubClient.Repositories.Get(ctx, repoOwner, repoName) diff --git a/cli/cmd/org/delete.go b/cli/cmd/org/delete.go index 801dc45cc44..6713a03547d 100644 --- a/cli/cmd/org/delete.go +++ b/cli/cmd/org/delete.go @@ -55,11 +55,7 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { if !force { fmt.Printf("Warn: Deleting the org %q will remove all metadata associated with the org\n", name) msg := fmt.Sprintf("Type %q to confirm deletion", name) - org, err := cmdutil.InputPrompt(msg, "") - if err != nil { - return err - } - + org := cmdutil.InputPrompt(msg, "") if org != name { return fmt.Errorf("Entered incorrect name: %q, expected value is %q", org, name) } diff --git a/cli/cmd/org/edit.go b/cli/cmd/org/edit.go index c0342c68163..c508af767e3 100644 --- a/cli/cmd/org/edit.go +++ b/cli/cmd/org/edit.go @@ -61,10 +61,7 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command { } if promptFlagValues { - description, err = cmdutil.InputPrompt("Enter the description", org.Description) - if err != nil { - return err - } + description := cmdutil.InputPrompt("Enter the description", org.Description) req.Description = &description } diff --git a/cli/cmd/project/delete.go b/cli/cmd/project/delete.go index 612b475e896..1a443b06a41 100644 --- a/cli/cmd/project/delete.go +++ b/cli/cmd/project/delete.go @@ -41,11 +41,7 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { ch.PrintfWarn("Warn: Deleting the project %q will remove all metadata associated with the project\n", name) msg := fmt.Sprintf("Type %q to confirm deletion", name) - project, err := cmdutil.InputPrompt(msg, "") - if err != nil { - return err - } - + project := cmdutil.InputPrompt(msg, "") if project != name { return fmt.Errorf("Entered incorrect name : %q, expected value is %q", project, name) } diff --git a/cli/cmd/project/edit.go b/cli/cmd/project/edit.go index 2149ce277c2..11654ed53f5 100644 --- a/cli/cmd/project/edit.go +++ b/cli/cmd/project/edit.go @@ -86,16 +86,10 @@ func EditCmd(ch *cmdutil.Helper) *cobra.Command { } proj := resp.Project - description, err = cmdutil.InputPrompt("Enter the description", proj.Description) - if err != nil { - return err - } + description = cmdutil.InputPrompt("Enter the description", proj.Description) req.Description = &description - prodBranch, err = cmdutil.InputPrompt("Enter the production branch", proj.ProdBranch) - if err != nil { - return err - } + prodBranch = cmdutil.InputPrompt("Enter the production branch", proj.ProdBranch) req.ProdBranch = &prodBranch public = cmdutil.ConfirmPrompt("Make project public", "", proj.Public) diff --git a/cli/pkg/cmdutil/prompt.go b/cli/pkg/cmdutil/prompt.go index e174d7974ca..db9cdf86188 100644 --- a/cli/pkg/cmdutil/prompt.go +++ b/cli/pkg/cmdutil/prompt.go @@ -48,7 +48,7 @@ func ConfirmPrompt(msg, help string, def bool) bool { return result } -func InputPrompt(msg, def string) (string, error) { +func InputPrompt(msg, def string) string { prompt := &survey.Input{ Message: msg, Default: def, @@ -56,9 +56,9 @@ func InputPrompt(msg, def string) (string, error) { result := def if err := survey.AskOne(prompt, &result); err != nil { fmt.Printf("Prompt failed %v\n", err) - return "", err + os.Exit(1) } - return strings.TrimSpace(result), nil + return strings.TrimSpace(result) } func StringPromptIfEmpty(input *string, msg string) { @@ -109,11 +109,7 @@ func SetFlagsByInputPrompts(cmd cobra.Command, flags ...string) error { val = fmt.Sprintf("%t", public) default: - val, err = InputPrompt(fmt.Sprintf("Enter the %s", f.Usage), f.DefValue) - if err != nil { - fmt.Println("error while input prompt, error:", err) - return - } + val = InputPrompt(fmt.Sprintf("Enter the %s", f.Usage), f.DefValue) } err = f.Value.Set(val) diff --git a/web-admin/src/routes/-/github/connect/+page.svelte b/web-admin/src/routes/-/github/connect/+page.svelte index aa9a9818307..bc308fec184 100644 --- a/web-admin/src/routes/-/github/connect/+page.svelte +++ b/web-admin/src/routes/-/github/connect/+page.svelte @@ -42,11 +42,13 @@ Rill projects deploy continuously when you push changes to Github. - - Please grant read-only access to your repository
-
+ {#if remote} + + Please grant access to your repository
+
+ {/if}
Connect to Github