Skip to content

Commit d3ee49c

Browse files
committed
feat(account): add login command
1 parent beebccd commit d3ee49c

File tree

5 files changed

+165
-0
lines changed

5 files changed

+165
-0
lines changed

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/UpCloudLtd/upcloud-go-api/v8 v8.14.0
88
github.com/adrg/xdg v0.3.2
99
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
10+
github.com/cli/browser v1.3.0
1011
github.com/gemalto/flume v0.12.0
1112
github.com/jedib0t/go-pretty/v6 v6.4.9
1213
github.com/m7shapan/cidr v0.0.0-20200427124835-7eba0889a5d2
@@ -15,17 +16,21 @@ require (
1516
github.com/spf13/pflag v1.0.5
1617
github.com/spf13/viper v1.7.1
1718
github.com/stretchr/testify v1.9.0
19+
github.com/zalando/go-keyring v0.2.6
1820
golang.org/x/crypto v0.31.0
1921
golang.org/x/term v0.27.0
2022
gopkg.in/yaml.v3 v3.0.1
2123
k8s.io/client-go v0.28.4
2224
)
2325

2426
require (
27+
al.essio.dev/pkg/shellescape v1.5.1 // indirect
2528
github.com/ansel1/merry v1.5.0 // indirect
29+
github.com/danieljoos/wincred v1.2.2 // indirect
2630
github.com/davecgh/go-spew v1.1.1 // indirect
2731
github.com/fsnotify/fsnotify v1.4.9 // indirect
2832
github.com/go-logr/logr v1.2.4 // indirect
33+
github.com/godbus/dbus/v5 v5.1.0 // indirect
2934
github.com/gogo/protobuf v1.3.2 // indirect
3035
github.com/golang/protobuf v1.5.3 // indirect
3136
github.com/google/gofuzz v1.2.0 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
2+
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
13
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
24
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
35
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -37,13 +39,17 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
3739
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
3840
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
3941
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
42+
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
43+
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
4044
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
4145
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
4246
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
4347
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
4448
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
4549
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
4650
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
51+
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
52+
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
4753
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4854
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4955
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -74,6 +80,8 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En
7480
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
7581
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
7682
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
83+
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
84+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
7785
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
7886
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
7987
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@@ -105,6 +113,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
105113
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
106114
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
107115
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
116+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
117+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
108118
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
109119
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
110120
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -273,6 +283,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
273283
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
274284
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
275285
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
286+
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
287+
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
276288
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
277289
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
278290
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=

internal/commands/account/login.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package account
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/UpCloudLtd/progress/messages"
7+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
8+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/commands/account/tokenreceiver"
9+
"github.com/UpCloudLtd/upcloud-cli/v3/internal/output"
10+
"github.com/zalando/go-keyring"
11+
)
12+
13+
// LoginCommand creates the "account login" command
14+
func LoginCommand() commands.Command {
15+
return &loginCommand{
16+
BaseCommand: commands.New(
17+
"login",
18+
"Configure a authentication token.",
19+
"upctl account login",
20+
),
21+
}
22+
}
23+
24+
type loginCommand struct {
25+
*commands.BaseCommand
26+
}
27+
28+
// ExecuteWithoutArguments implements commands.NoArgumentCommand
29+
func (s *loginCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) {
30+
msg := "Waiting to receive token from browser."
31+
exec.PushProgressStarted(msg)
32+
33+
receiver := tokenreceiver.New()
34+
err := receiver.Start()
35+
if err != nil {
36+
return commands.HandleError(exec, msg, err)
37+
}
38+
39+
err = receiver.OpenBrowser()
40+
if err != nil {
41+
url := receiver.GetLoginURL()
42+
exec.PushProgressUpdate(messages.Update{
43+
Message: "Failed to open browser.",
44+
Status: messages.MessageStatusError,
45+
Details: fmt.Sprintf("Please open a browser and navigate to %s to continue with the login.", url),
46+
})
47+
}
48+
49+
token, err := receiver.Wait(exec.Context())
50+
if err != nil {
51+
return commands.HandleError(exec, msg, err)
52+
}
53+
54+
err = keyring.Set("UpCloud", "", token)
55+
if err != nil {
56+
return commands.HandleError(exec, msg, fmt.Errorf("failed to save token to keyring: %w", err))
57+
}
58+
59+
exec.PushProgressSuccess(msg)
60+
61+
return output.None{}, nil
62+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package tokenreceiver
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"time"
9+
10+
"github.com/cli/browser"
11+
)
12+
13+
type ReceiverServer struct {
14+
server *http.Server
15+
token string
16+
port string
17+
}
18+
19+
func New() *ReceiverServer {
20+
return &ReceiverServer{}
21+
}
22+
23+
func getPort(listener net.Listener) string {
24+
_, port, _ := net.SplitHostPort(listener.Addr().String())
25+
return port
26+
}
27+
28+
func getURL(target string) string {
29+
return fmt.Sprintf("http://localhost:3000/account/upctl-login/%s", target)
30+
}
31+
32+
func (s *ReceiverServer) GetLoginURL() string {
33+
return getURL(s.port)
34+
}
35+
36+
func (s *ReceiverServer) Start() error {
37+
handler := http.NewServeMux()
38+
handler.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
39+
token := req.URL.Query().Get("token")
40+
if token == "" {
41+
http.Redirect(w, req, getURL("error"), http.StatusSeeOther)
42+
return
43+
}
44+
s.token = token
45+
http.Redirect(w, req, getURL("success"), http.StatusSeeOther)
46+
})
47+
48+
listener, err := net.Listen("tcp", "127.0.0.1:0")
49+
if err != nil {
50+
return fmt.Errorf("failed to create receiver server: %w", err)
51+
}
52+
53+
go func() {
54+
defer listener.Close()
55+
s.server = &http.Server{
56+
Handler: handler,
57+
ReadHeaderTimeout: time.Second,
58+
}
59+
_ = s.server.Serve(listener)
60+
}()
61+
s.port = getPort(listener)
62+
return nil
63+
}
64+
65+
func (s *ReceiverServer) OpenBrowser() error {
66+
return browser.OpenURL(s.GetLoginURL())
67+
}
68+
69+
func (s *ReceiverServer) Wait(ctx context.Context) (string, error) {
70+
ticker := time.NewTicker(time.Second * 2)
71+
defer ticker.Stop()
72+
73+
for {
74+
select {
75+
case <-ctx.Done():
76+
_ = s.server.Shutdown(context.TODO())
77+
return "", ctx.Err()
78+
case <-ticker.C:
79+
if s.token != "" {
80+
_ = s.server.Shutdown(context.TODO())
81+
return s.token, nil
82+
}
83+
}
84+
}
85+
}

internal/commands/all/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func BuildCommands(rootCmd *cobra.Command, conf *config.Config) {
115115
// Account
116116
accountCommand := commands.BuildCommand(account.BaseAccountCommand(), rootCmd, conf)
117117
commands.BuildCommand(account.ShowCommand(), accountCommand.Cobra(), conf)
118+
commands.BuildCommand(account.LoginCommand(), accountCommand.Cobra(), conf)
118119
commands.BuildCommand(account.ListCommand(), accountCommand.Cobra(), conf)
119120
commands.BuildCommand(account.DeleteCommand(), accountCommand.Cobra(), conf)
120121

0 commit comments

Comments
 (0)