Skip to content

Commit

Permalink
login and connect to github in one flow
Browse files Browse the repository at this point in the history
  • Loading branch information
k-anshul committed Apr 3, 2024
1 parent 4f3fdfd commit e204b05
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 84 deletions.
37 changes: 28 additions & 9 deletions admin/server/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
96 changes: 56 additions & 40 deletions cli/cmd/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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)
}
}
}

Expand Down Expand Up @@ -438,19 +433,37 @@ 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)
if err != nil {
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 {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
Expand Down
6 changes: 1 addition & 5 deletions cli/cmd/org/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
5 changes: 1 addition & 4 deletions cli/cmd/org/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
6 changes: 1 addition & 5 deletions cli/cmd/project/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
10 changes: 2 additions & 8 deletions cli/cmd/project/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 4 additions & 8 deletions cli/pkg/cmdutil/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ 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,
}
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) {
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 7 additions & 5 deletions web-admin/src/routes/-/github/connect/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@
<CtaMessage>
Rill projects deploy continuously when you push changes to Github.
</CtaMessage>
<CtaMessage>
Please grant read-only access to your repository<br /><GithubRepoInline
githubUrl={remote}
/>
</CtaMessage>
{#if remote}
<CtaMessage>
Please grant access to your repository<br /><GithubRepoInline
githubUrl={remote}
/>
</CtaMessage>
{/if}
<div class="mt-4 w-full">
<CtaButton variant="primary" on:click={handleGoToGithub}
>Connect to Github</CtaButton
Expand Down

0 comments on commit e204b05

Please sign in to comment.