Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7f226d4

Browse files
authored
feat: add support for coder_git_auth data source (#6334)
* Add git auth providers schema * Pipe git auth providers to the schema * Add git auth providers to the API * Add gitauth endpoint to query authenticated state * Add endpoint to query git state * Use BroadcastChannel to automatically authenticate with Git * Add error validation for submitting the create workspace form * Fix panic on template dry-run * Add tests for the template version Git auth endpoint * Show error if no gitauth is configured * Add gitauth to cliui * Fix unused method receiver * Fix linting errors * Fix dbauthz querier test * Fix make gen * Add JavaScript test for git auth * Fix bad error message * Fix provisionerd test race See https://github.com/coder/coder/actions/runs/4277960646/jobs/7447232814 * Fix requested changes * Add comment to CreateWorkspacePageView
1 parent 3d8b77d commit 7f226d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2768
-841
lines changed

cli/cliui/gitauth.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package cliui
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"time"
8+
9+
"github.com/briandowns/spinner"
10+
11+
"github.com/coder/coder/codersdk"
12+
)
13+
14+
type GitAuthOptions struct {
15+
Fetch func(context.Context) ([]codersdk.TemplateVersionGitAuth, error)
16+
FetchInterval time.Duration
17+
}
18+
19+
func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
20+
if opts.FetchInterval == 0 {
21+
opts.FetchInterval = 500 * time.Millisecond
22+
}
23+
gitAuth, err := opts.Fetch(ctx)
24+
if err != nil {
25+
return err
26+
}
27+
28+
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
29+
spin.Writer = writer
30+
spin.ForceOutput = true
31+
spin.Suffix = " Waiting for Git authentication..."
32+
defer spin.Stop()
33+
34+
ticker := time.NewTicker(opts.FetchInterval)
35+
defer ticker.Stop()
36+
for _, auth := range gitAuth {
37+
if auth.Authenticated {
38+
return nil
39+
}
40+
41+
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.Type.Pretty(), auth.AuthenticateURL)
42+
43+
ticker.Reset(opts.FetchInterval)
44+
spin.Start()
45+
for {
46+
select {
47+
case <-ctx.Done():
48+
return ctx.Err()
49+
case <-ticker.C:
50+
}
51+
gitAuth, err := opts.Fetch(ctx)
52+
if err != nil {
53+
return err
54+
}
55+
var authed bool
56+
for _, a := range gitAuth {
57+
if !a.Authenticated || a.ID != auth.ID {
58+
continue
59+
}
60+
authed = true
61+
break
62+
}
63+
// The user authenticated with the provider!
64+
if authed {
65+
break
66+
}
67+
}
68+
spin.Stop()
69+
_, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.Type.Pretty())
70+
}
71+
return nil
72+
}

cli/cliui/gitauth_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cliui_test
2+
3+
import (
4+
"context"
5+
"net/url"
6+
"sync/atomic"
7+
"testing"
8+
"time"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/stretchr/testify/assert"
12+
13+
"github.com/coder/coder/cli/cliui"
14+
"github.com/coder/coder/codersdk"
15+
"github.com/coder/coder/pty/ptytest"
16+
"github.com/coder/coder/testutil"
17+
)
18+
19+
func TestGitAuth(t *testing.T) {
20+
t.Parallel()
21+
22+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
23+
defer cancel()
24+
25+
ptty := ptytest.New(t)
26+
cmd := &cobra.Command{
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
var fetched atomic.Bool
29+
return cliui.GitAuth(cmd.Context(), cmd.OutOrStdout(), cliui.GitAuthOptions{
30+
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
31+
defer fetched.Store(true)
32+
return []codersdk.TemplateVersionGitAuth{{
33+
ID: "github",
34+
Type: codersdk.GitProviderGitHub,
35+
Authenticated: fetched.Load(),
36+
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
37+
}}, nil
38+
},
39+
FetchInterval: time.Millisecond,
40+
})
41+
},
42+
}
43+
cmd.SetOutput(ptty.Output())
44+
cmd.SetIn(ptty.Input())
45+
done := make(chan struct{})
46+
go func() {
47+
defer close(done)
48+
err := cmd.Execute()
49+
assert.NoError(t, err)
50+
}()
51+
ptty.ExpectMatchContext(ctx, "You must authenticate with")
52+
ptty.ExpectMatchContext(ctx, "https://example.com/gitauth/github")
53+
ptty.ExpectMatchContext(ctx, "Successfully authenticated with GitHub")
54+
<-done
55+
}

cli/create.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"context"
45
"fmt"
56
"io"
67
"time"
@@ -324,6 +325,15 @@ PromptRichParamLoop:
324325
_, _ = fmt.Fprintln(cmd.OutOrStdout())
325326
}
326327

328+
err = cliui.GitAuth(ctx, cmd.OutOrStdout(), cliui.GitAuthOptions{
329+
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
330+
return client.TemplateVersionGitAuth(ctx, templateVersion.ID)
331+
},
332+
})
333+
if err != nil {
334+
return nil, xerrors.Errorf("template version git auth: %w", err)
335+
}
336+
327337
// Run a dry-run with the given parameters to check correctness
328338
dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
329339
WorkspaceName: args.NewWorkspaceName,

cli/create_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ package cli_test
33
import (
44
"context"
55
"fmt"
6+
"net/http"
7+
"net/url"
68
"os"
9+
"regexp"
710
"testing"
811
"time"
912

1013
"github.com/stretchr/testify/assert"
1114
"github.com/stretchr/testify/require"
15+
"golang.org/x/oauth2"
1216

1317
"github.com/coder/coder/cli/clitest"
1418
"github.com/coder/coder/coderd/coderdtest"
19+
"github.com/coder/coder/coderd/database"
20+
"github.com/coder/coder/coderd/gitauth"
1521
"github.com/coder/coder/codersdk"
1622
"github.com/coder/coder/provisioner/echo"
1723
"github.com/coder/coder/provisionersdk/proto"
@@ -603,6 +609,61 @@ func TestCreateValidateRichParameters(t *testing.T) {
603609
})
604610
}
605611

612+
func TestCreateWithGitAuth(t *testing.T) {
613+
t.Parallel()
614+
echoResponses := &echo.Responses{
615+
Parse: echo.ParseComplete,
616+
ProvisionPlan: []*proto.Provision_Response{
617+
{
618+
Type: &proto.Provision_Response_Complete{
619+
Complete: &proto.Provision_Complete{
620+
GitAuthProviders: []string{"github"},
621+
},
622+
},
623+
},
624+
},
625+
ProvisionApply: []*proto.Provision_Response{{
626+
Type: &proto.Provision_Response_Complete{
627+
Complete: &proto.Provision_Complete{},
628+
},
629+
}},
630+
}
631+
632+
client := coderdtest.New(t, &coderdtest.Options{
633+
GitAuthConfigs: []*gitauth.Config{{
634+
OAuth2Config: &oauth2Config{},
635+
ID: "github",
636+
Regex: regexp.MustCompile(`github\.com`),
637+
Type: codersdk.GitProviderGitHub,
638+
}},
639+
IncludeProvisionerDaemon: true,
640+
})
641+
user := coderdtest.CreateFirstUser(t, client)
642+
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
643+
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
644+
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
645+
646+
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
647+
clitest.SetupConfig(t, client, root)
648+
doneChan := make(chan struct{})
649+
pty := ptytest.New(t)
650+
cmd.SetIn(pty.Input())
651+
cmd.SetOut(pty.Output())
652+
go func() {
653+
defer close(doneChan)
654+
err := cmd.Execute()
655+
assert.NoError(t, err)
656+
}()
657+
658+
pty.ExpectMatch("You must authenticate with GitHub to create a workspace")
659+
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
660+
_ = resp.Body.Close()
661+
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
662+
pty.ExpectMatch("Confirm create?")
663+
pty.WriteLine("yes")
664+
<-doneChan
665+
}
666+
606667
func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
607668
return []*proto.Parse_Response{{
608669
Type: &proto.Parse_Response_Complete{
@@ -638,3 +699,31 @@ func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Resp
638699
},
639700
}}
640701
}
702+
703+
type oauth2Config struct{}
704+
705+
func (*oauth2Config) AuthCodeURL(state string, _ ...oauth2.AuthCodeOption) string {
706+
return "/?state=" + url.QueryEscape(state)
707+
}
708+
709+
func (*oauth2Config) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
710+
return &oauth2.Token{
711+
AccessToken: "token",
712+
RefreshToken: "refresh",
713+
Expiry: database.Now().Add(time.Hour),
714+
}, nil
715+
}
716+
717+
func (*oauth2Config) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource {
718+
return &oauth2TokenSource{}
719+
}
720+
721+
type oauth2TokenSource struct{}
722+
723+
func (*oauth2TokenSource) Token() (*oauth2.Token, error) {
724+
return &oauth2.Token{
725+
AccessToken: "token",
726+
RefreshToken: "refresh",
727+
Expiry: database.Now().Add(time.Hour),
728+
}, nil
729+
}

cmd/cliui/main.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"net/url"
89
"os"
910
"strings"
11+
"sync/atomic"
1012
"time"
1113

1214
"github.com/spf13/cobra"
@@ -235,6 +237,41 @@ func main() {
235237
},
236238
})
237239

240+
root.AddCommand(&cobra.Command{
241+
Use: "git-auth",
242+
RunE: func(cmd *cobra.Command, args []string) error {
243+
var count atomic.Int32
244+
var githubAuthed atomic.Bool
245+
var gitlabAuthed atomic.Bool
246+
go func() {
247+
// Sleep to display the loading indicator.
248+
time.Sleep(time.Second)
249+
// Swap to true to display success and move onto GitLab.
250+
githubAuthed.Store(true)
251+
// Show the loading indicator again...
252+
time.Sleep(time.Second * 2)
253+
// Complete the auth!
254+
gitlabAuthed.Store(true)
255+
}()
256+
return cliui.GitAuth(cmd.Context(), cmd.OutOrStdout(), cliui.GitAuthOptions{
257+
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
258+
count.Add(1)
259+
return []codersdk.TemplateVersionGitAuth{{
260+
ID: "github",
261+
Type: codersdk.GitProviderGitHub,
262+
Authenticated: githubAuthed.Load(),
263+
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
264+
}, {
265+
ID: "gitlab",
266+
Type: codersdk.GitProviderGitLab,
267+
Authenticated: gitlabAuthed.Load(),
268+
AuthenticateURL: "https://example.com/gitauth/gitlab?redirect=" + url.QueryEscape("/gitauth?notify"),
269+
}}, nil
270+
},
271+
})
272+
},
273+
})
274+
238275
err := root.Execute()
239276
if err != nil {
240277
_, _ = fmt.Println(err.Error())

0 commit comments

Comments
 (0)