Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/local-app/cmd/login.go
3601 views
1
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package cmd
6
7
import (
8
"context"
9
"errors"
10
"fmt"
11
"log/slog"
12
"net/url"
13
"os"
14
"strings"
15
"time"
16
17
"github.com/bufbuild/connect-go"
18
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
19
"github.com/gitpod-io/local-app/pkg/auth"
20
"github.com/gitpod-io/local-app/pkg/config"
21
"github.com/gitpod-io/local-app/pkg/prettyprint"
22
"github.com/manifoldco/promptui"
23
"github.com/spf13/cobra"
24
)
25
26
var loginOpts struct {
27
Token string
28
Host string
29
ContextName string
30
OrganizationID string
31
NonInteractive bool
32
}
33
34
// loginCmd represents the login command
35
var loginCmd = &cobra.Command{
36
Use: "login",
37
Short: "Logs the user in to the CLI",
38
Long: `Logs the user in and stores the token in the system keychain.`,
39
Args: cobra.ExactArgs(0),
40
RunE: func(cmd *cobra.Command, args []string) error {
41
cmd.SilenceUsage = true
42
43
if !strings.HasPrefix(loginOpts.Host, "http") {
44
loginOpts.Host = "https://" + loginOpts.Host
45
}
46
host, err := url.Parse(loginOpts.Host)
47
if err != nil {
48
return fmt.Errorf("cannot parse host %s: %w", loginOpts.Host, err)
49
}
50
51
token := loginOpts.Token
52
if token == "" {
53
token = os.Getenv("GITPOD_TOKEN")
54
}
55
if token == "" {
56
if loginOpts.NonInteractive {
57
return fmt.Errorf("no token provided")
58
} else {
59
var err error
60
token, err = auth.Login(context.Background(), auth.LoginOpts{
61
GitpodURL: loginOpts.Host,
62
AuthTimeout: 5 * time.Minute,
63
// Request CLI scopes (extended compared to the local companion app)
64
ExtendScopes: true,
65
})
66
if err != nil {
67
return err
68
}
69
}
70
}
71
72
cfg := config.FromContext(cmd.Context())
73
gpctx := &config.ConnectionContext{
74
Host: &config.YamlURL{URL: host},
75
OrganizationID: loginOpts.OrganizationID,
76
}
77
78
err = auth.SetToken(loginOpts.Host, token)
79
if err != nil {
80
if slog.Default().Enabled(cmd.Context(), slog.LevelDebug) {
81
slog.Debug("could not write token to keyring, storing in config file instead", "err", err)
82
} else {
83
slog.Warn("could not write token to keyring, storing in config file instead. Use -v to see the error.")
84
}
85
gpctx.Token = token
86
}
87
88
contextName := loginOpts.ContextName
89
if _, exists := cfg.Contexts[contextName]; exists && !cmd.Flags().Changed("context-name") {
90
contextName = host.Hostname()
91
}
92
cfg.Contexts[contextName] = gpctx
93
cfg.ActiveContext = contextName
94
95
if loginOpts.OrganizationID == "" {
96
clnt, err := getGitpodClient(config.ToContext(context.Background(), cfg))
97
if err != nil {
98
return fmt.Errorf("cannot connect to Gitpod with this context: %w", err)
99
}
100
if !loginOpts.NonInteractive {
101
fmt.Println("loading your organizations...")
102
}
103
orgsList, err := clnt.Teams.ListTeams(cmd.Context(), connect.NewRequest(&v1.ListTeamsRequest{}))
104
if err != nil {
105
var (
106
resolutions []string
107
unauthenticated bool
108
)
109
if ce := new(connect.Error); errors.As(err, &ce) && ce.Code() == connect.CodeUnauthenticated {
110
unauthenticated = true
111
resolutions = []string{
112
"pass an organization ID using --organization-id",
113
}
114
if loginOpts.Token != "" {
115
resolutions = append(resolutions,
116
"make sure the token has the right scopes",
117
"use a different token",
118
"login without passing a token but using the browser instead",
119
)
120
}
121
}
122
if unauthenticated {
123
return prettyprint.AddResolution(fmt.Errorf("unauthenticated"), resolutions...)
124
} else {
125
return prettyprint.MarkExceptional(err)
126
}
127
}
128
129
orgs := orgsList.Msg.GetTeams()
130
fmt.Print("\033[A\033[K")
131
132
resolutions := []string{
133
"pass an organization ID using --organization-id",
134
}
135
136
var orgID string
137
switch len(orgs) {
138
case 0:
139
return prettyprint.AddResolution(fmt.Errorf("no organizations found"), resolutions...)
140
case 1:
141
orgID = orgs[0].Id
142
default:
143
if loginOpts.NonInteractive {
144
resolutions = append(resolutions,
145
"omit --non-interactive and select an organization interactively",
146
)
147
return prettyprint.AddResolution(fmt.Errorf("found more than one organization"), resolutions...)
148
}
149
150
var orgNames []string
151
for _, org := range orgs {
152
orgNames = append(orgNames, org.Name)
153
}
154
155
prompt := promptui.Select{
156
Label: "What organization would you like to use?",
157
Items: orgNames,
158
Templates: &promptui.SelectTemplates{
159
Selected: "Selected organization {{ . }}",
160
},
161
}
162
selectedIndex, selectedValue, err := prompt.Run()
163
if selectedValue == "" {
164
return fmt.Errorf("no organization selected")
165
}
166
if err != nil {
167
return err
168
}
169
orgID = orgs[selectedIndex].Id
170
}
171
cfg.Contexts[contextName].OrganizationID = orgID
172
}
173
174
err = config.SaveConfig(cfg.Filename, cfg)
175
if err != nil {
176
return err
177
}
178
179
client, err := getGitpodClient(config.ToContext(cmd.Context(), cfg))
180
if err != nil {
181
return err
182
}
183
who, err := whoami(cmd.Context(), client, gpctx)
184
if err != nil {
185
return err
186
}
187
188
slog.Info("login successful")
189
fmt.Println()
190
return WriteTabular(who, formatOpts{}, prettyprint.WriterFormatNarrow)
191
},
192
}
193
194
func init() {
195
rootCmd.AddCommand(loginCmd)
196
197
host := "https://gitpod.io"
198
if v := os.Getenv("GITPOD_HOST"); v != "" {
199
host = v
200
}
201
loginCmd.Flags().StringVar(&loginOpts.Host, "host", host, "The Gitpod instance to log in to (defaults to $GITPOD_HOST)")
202
loginCmd.Flags().StringVar(&loginOpts.Token, "token", "", "The token to use for authentication (defaults to $GITPOD_TOKEN)")
203
loginCmd.Flags().StringVarP(&loginOpts.ContextName, "context-name", "n", "default", "The name of the context to create")
204
loginCmd.Flags().StringVar(&loginOpts.OrganizationID, "org", "", "The organization ID to use for the context")
205
loginCmd.Flags().BoolVar(&loginOpts.NonInteractive, "non-interactive", false, "Disable opening the browser and prompt to select an organization")
206
}
207
208