Skip to content

Commit

Permalink
cli: Cloud traces support via dagger login
Browse files Browse the repository at this point in the history
Signed-off-by: Andrea Luzzardi <[email protected]>
  • Loading branch information
aluzzardi committed Apr 18, 2024
1 parent 0aba66c commit ef46c5b
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 135 deletions.
56 changes: 41 additions & 15 deletions cmd/dagger/cloud.go
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"fmt"
"os"

"github.com/spf13/cobra"
Expand All @@ -13,7 +14,6 @@ import (
func init() {
cloud := &CloudCLI{}

rootCmd.PersistentFlags().StringVar(&cloud.API, "api", "https://api.dagger.cloud", "Dagger Cloud API URL")
rootCmd.PersistentFlags().MarkHidden("api")

group := &cobra.Group{
Expand All @@ -23,7 +23,7 @@ func init() {
rootCmd.AddGroup(group)

loginCmd := &cobra.Command{
Use: "login",
Use: "login [flags] [ORG]",
Short: "Log in to Dagger Cloud",
GroupID: group.ID,
RunE: cloud.Login,
Expand All @@ -40,16 +40,19 @@ func init() {
}

type CloudCLI struct {
API string
}

func (cli *CloudCLI) Client(ctx context.Context) (*cloud.Client, error) {
return cloud.NewClient(ctx, cli.API)
return cloud.NewClient(ctx)
}

func (cli *CloudCLI) Login(cmd *cobra.Command, args []string) error {
lg := Logger(os.Stderr)
ctx := lg.WithContext(cmd.Context())
ctx := cmd.Context()

var orgName string
if len(args) > 0 {
orgName = args[0]
}

if err := auth.Login(ctx); err != nil {
return err
Expand All @@ -64,18 +67,41 @@ func (cli *CloudCLI) Login(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
var orgID string
switch len(user.Orgs) {
case 0:
fmt.Fprintf(os.Stderr, "You are not a member of any organizations.\n")
os.Exit(1)
case 1:
orgID = user.Orgs[0].ID
default:
if orgName == "" {
fmt.Fprintf(os.Stderr, "You are a member of multiple organizations. Please select one with `dagger cloud login ORG`:\n\n")
for _, org := range user.Orgs {
fmt.Fprintf(os.Stderr, "- %s\n", org.Name)
}
os.Exit(1)
}
for _, org := range user.Orgs {
if org.Name == orgName {
orgID = org.ID
break
}
}
if orgID == "" {
fmt.Fprintf(os.Stderr, "Organization %s not found\n", orgName)
os.Exit(1)
}
}

lg.Info().Str("user", user.ID).Msg("logged in")
return nil
}

func (cli *CloudCLI) Logout(cmd *cobra.Command, args []string) error {
lg := Logger(os.Stderr)

if err := auth.Logout(); err != nil {
if err := auth.SetOrg(orgID); err != nil {
return err
}

lg.Info().Msg("logged out")
fmt.Fprintf(os.Stderr, "Success.\n")
return nil
}

func (cli *CloudCLI) Logout(cmd *cobra.Command, args []string) error {
return auth.Logout()
}
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -92,7 +92,7 @@ require (
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
golang.org/x/mod v0.17.0
golang.org/x/net v0.24.0
golang.org/x/oauth2 v0.17.0
golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.19.0
golang.org/x/term v0.19.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -824,6 +824,8 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down
154 changes: 43 additions & 111 deletions internal/cloud/auth/auth.go
Expand Up @@ -2,126 +2,53 @@ package auth

import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"

"github.com/adrg/xdg"
"github.com/pkg/browser"
"github.com/rs/zerolog/log"
"golang.org/x/oauth2"
)

const (
authDomain = "auth.dagger.cloud"
callbackPort = 38932
authDomain = "https://auth.dagger.cloud"
)

var credentialsFile = filepath.Join(xdg.ConfigHome, "dagger", "credentials.json")
var (
configRoot = filepath.Join(xdg.ConfigHome, "dagger")
credentialsFile = filepath.Join(configRoot, "credentials.json")
orgFile = filepath.Join(configRoot, "org")
)

var authConfig = &oauth2.Config{
// https://manage.auth0.com/dashboard/us/dagger-io/applications/brEY7u4SEoFypOgYBdYMs32b4ShRVIEv/settings
ClientID: "brEY7u4SEoFypOgYBdYMs32b4ShRVIEv",
RedirectURL: fmt.Sprintf("http://localhost:%d/callback", callbackPort),
Scopes: []string{"openid", "offline_access"},
ClientID: "brEY7u4SEoFypOgYBdYMs32b4ShRVIEv",
Scopes: []string{"openid", "offline_access"},
Endpoint: oauth2.Endpoint{
AuthStyle: oauth2.AuthStyleInParams,
AuthURL: "https://" + authDomain + "/authorize",
TokenURL: "https://" + authDomain + "/oauth/token",
AuthStyle: oauth2.AuthStyleInParams,
AuthURL: authDomain + "/authorize",
TokenURL: authDomain + "/oauth/token",
DeviceAuthURL: authDomain + "/oauth/device/code",
},
}

// Login logs the user in and stores the credentials for later use.
// Interactive messages are printed to w.
func Login(ctx context.Context) error {
lg := log.Ctx(ctx)

lg.Info().Msg("logging in to " + authDomain)

// oauth2 localhost handler
requestCh := make(chan *http.Request)

m := http.NewServeMux()
// since Login could be called multiple times, only register /callback once
m.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
if oauthError := r.URL.Query().Get("error"); oauthError != "" {
message := r.URL.Query().Get("error_description")
fmt.Fprintf(w, `
<html>
<head>
<script>window.close()</script>
<body>
%s
</body>
</html>
`, message)
} else {
fmt.Fprint(w, `
<html>
<head>
<script>window.location.href="https://dagger.cloud/auth-success"</script>
<body>
</body>
</html>
`)
}

requestCh <- r
})

srv := &http.Server{ //nolint: gosec
Addr: fmt.Sprintf("localhost:%d", callbackPort),
Handler: m,
}

go func() {
err := srv.ListenAndServe()
if err != http.ErrServerClosed {
lg.Fatal().Err(err).Msg("auth server failed")
}
}()

defer srv.Shutdown(ctx)

// Generate random state
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return fmt.Errorf("rand: %w", err)
}
state := hex.EncodeToString(b)

tokenURL := authConfig.AuthCodeURL(state)

lg.Info().Msgf("opening %s", tokenURL)

if err := browser.OpenURL(tokenURL); err != nil {
lg.Warn().Err(err).Msg("could not open browser; please follow the above URL manually")
}

var req *http.Request
select {
case req = <-requestCh:
case <-ctx.Done():
lg.Info().Msg("giving up")
// If the user is already authenticated, skip the login process.
if _, err := Token(ctx); err == nil {
return nil
}

responseState := req.URL.Query().Get("state")
if state != responseState {
return fmt.Errorf("corrupted login challenge (%q != %q)", state, responseState)
deviceAuth, err := authConfig.DeviceAuth(ctx)
if err != nil {
return err
}

if oauthError := req.URL.Query().Get("error"); oauthError != "" {
description := req.URL.Query().Get("error_description")
return fmt.Errorf("authentication error: %s (%s)", oauthError, description)
}
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\t%s\n\n", deviceAuth.VerificationURIComplete)

token, err := authConfig.Exchange(ctx, req.URL.Query().Get("code"))
token, err := authConfig.DeviceAccessToken(ctx, deviceAuth)
if err != nil {
return err
}
Expand All @@ -138,35 +65,40 @@ func Logout() error {
return err
}

func TokenSource(ctx context.Context) oauth2.TokenSource {
return loginTokenSource{ctx}
}
func TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
token, err := loadCredentials()
if err != nil {
return nil, err
}

// loginTokenSource is a TokenSource that will re-login if a token is not available or cannot be refreshed.
type loginTokenSource struct {
ctx context.Context
return authConfig.TokenSource(ctx, token), nil
}

func (src loginTokenSource) Token() (*oauth2.Token, error) {
token, err := loadCredentials()
func Token(ctx context.Context) (*oauth2.Token, error) {
tokenSource, err := TokenSource(ctx)
if err != nil {
if err := Login(src.ctx); err != nil {
return nil, err
}

return src.Token()
return nil, err
}
return tokenSource.Token()
}

token, err = authConfig.TokenSource(src.ctx, token).Token()
func Org() (string, error) {
data, err := os.ReadFile(orgFile)
if err != nil {
if err := Login(src.ctx); err != nil {
return nil, err
}
return "", err
}
return string(data), nil
}

return src.Token()
func SetOrg(org string) error {
if err := os.MkdirAll(filepath.Dir(orgFile), 0o755); err != nil {
return err
}

return token, nil
if err := os.WriteFile(orgFile, []byte(org), 0o600); err != nil {
return err
}
return nil
}

func loadCredentials() (*oauth2.Token, error) {
Expand Down
23 changes: 18 additions & 5 deletions internal/cloud/client.go
Expand Up @@ -3,6 +3,7 @@ package cloud
import (
"context"
"net/url"
"os"

"github.com/shurcooL/graphql"
"golang.org/x/oauth2"
Expand All @@ -15,26 +16,38 @@ type Client struct {
c *graphql.Client
}

func NewClient(ctx context.Context, api string) (*Client, error) {
if api == "" {
api = "https://api.dagger.cloud"
func NewClient(ctx context.Context) (*Client, error) {
api := "https://api.dagger.cloud"
if cloudURL := os.Getenv("DAGGER_CLOUD_URL"); cloudURL != "" {
api = cloudURL
}

u, err := url.Parse(api)
if err != nil {
return nil, err
}

httpClient := oauth2.NewClient(ctx, auth.TokenSource(ctx))
tokenSource, err := auth.TokenSource(ctx)
if err != nil {
return nil, err
}

httpClient := oauth2.NewClient(ctx, tokenSource)

return &Client{
u: u,
c: graphql.NewClient(api+"/query", httpClient),
}, nil
}

type OrgResponse struct {
ID string
Name string
}

type UserResponse struct {
ID string
ID string
Orgs []OrgResponse
}

func (c *Client) User(ctx context.Context) (*UserResponse, error) {
Expand Down

0 comments on commit ef46c5b

Please sign in to comment.