Skip to content

Commit 642b368

Browse files
authored
feat: add login command (#4043)
1 parent c5f01ed commit 642b368

File tree

9 files changed

+373
-7
lines changed

9 files changed

+373
-7
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Start an interactive connection to scaleway to initialize the active profile of the config
4+
A webpage will open while the CLI will wait for a response.
5+
Once you connected to Scaleway, the profile should be configured.
6+
7+
USAGE:
8+
scw login [arg=value ...]
9+
10+
ARGS:
11+
[port] The port number used to wait for browser's response
12+
13+
FLAGS:
14+
-h, --help help for login
15+
16+
GLOBAL FLAGS:
17+
-c, --config string The path to the config file
18+
-D, --debug Enable debug mode
19+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
20+
-p, --profile string The config profile to use
21+
22+
SEE ALSO:
23+
# Init profile manually
24+
scw init

cmd/scw/testdata/test-main-usage-usage.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ CONFIGURATION COMMANDS:
4949
config Config file management
5050
info Get info about current settings
5151
init Initialize the config
52+
login Login to scaleway
5253

5354
UTILITY COMMANDS:
5455
feedback Send feedback to the Scaleway CLI Team!

docs/commands/login.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!-- DO NOT EDIT: this file is automatically generated using scw-doc-gen -->
2+
# Documentation for `scw login`
3+
Start an interactive connection to scaleway to initialize the active profile of the config
4+
A webpage will open while the CLI will wait for a response.
5+
Once you connected to Scaleway, the profile should be configured.
6+
7+
8+
9+

internal/namespaces/get_commands.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1"
3535
keymanager "github.com/scaleway/scaleway-cli/v2/internal/namespaces/key_manager/v1alpha1"
3636
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/lb/v1"
37+
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/login"
3738
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/marketplace/v2"
3839
mnq "github.com/scaleway/scaleway-cli/v2/internal/namespaces/mnq/v1beta1"
3940
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/object/v1"
@@ -104,6 +105,7 @@ func GetCommands() *core.Commands {
104105
jobs.GetCommands(),
105106
serverless_sqldb.GetCommands(),
106107
edgeservices.GetCommands(),
108+
login.GetCommands(),
107109
)
108110

109111
if beta {

internal/namespaces/init/init.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ See below the schema `scw init` follows to ask for default config:
5454
*/
5555

5656
func GetCommands() *core.Commands {
57-
return core.NewCommands(initCommand())
57+
return core.NewCommands(Command())
5858
}
5959

60-
type initArgs struct {
60+
type Args struct {
6161
AccessKey string
6262
SecretKey string
6363
ProjectID string
@@ -70,7 +70,7 @@ type initArgs struct {
7070
InstallAutocomplete *bool
7171
}
7272

73-
func initCommand() *core.Command {
73+
func Command() *core.Command {
7474
return &core.Command{
7575
Groups: []string{"config"},
7676
Short: `Initialize the config`,
@@ -83,7 +83,7 @@ Default path for configuration file is based on the following priority order:
8383
- $USERPROFILE/.config/scw/config.yaml`,
8484
Namespace: "init",
8585
AllowAnonymousClient: true,
86-
ArgsType: reflect.TypeOf(initArgs{}),
86+
ArgsType: reflect.TypeOf(Args{}),
8787
ArgSpecs: core.ArgSpecs{
8888
{
8989
Name: "secret-key",
@@ -126,9 +126,13 @@ Default path for configuration file is based on the following priority order:
126126
Short: "Config management help",
127127
Command: "scw config",
128128
},
129+
{
130+
Short: "Login through a web page",
131+
Command: "scw login",
132+
},
129133
},
130134
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
131-
args := argsI.(*initArgs)
135+
args := argsI.(*Args)
132136

133137
profileName := core.ExtractProfileName(ctx)
134138
configPath := core.ExtractConfigPath(ctx)
@@ -243,7 +247,7 @@ Default path for configuration file is based on the following priority order:
243247
successDetails := []string(nil)
244248

245249
// Install autocomplete
246-
if *args.InstallAutocomplete {
250+
if args.InstallAutocomplete != nil && *args.InstallAutocomplete {
247251
_, _ = interactive.Println()
248252
_, err := autocomplete.InstallCommandRun(ctx, &autocomplete.InstallArgs{
249253
Basename: "scw",
@@ -254,7 +258,7 @@ Default path for configuration file is based on the following priority order:
254258
}
255259

256260
// Init SSH Key
257-
if *args.WithSSHKey {
261+
if args.WithSSHKey != nil && *args.WithSSHKey {
258262
_, _ = interactive.Println()
259263
_, err := iamcommands.InitWithSSHKeyRun(ctx, nil)
260264
if err != nil {

internal/namespaces/login/login.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package login
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"reflect"
9+
"time"
10+
11+
"github.com/scaleway/scaleway-cli/v2/internal/core"
12+
initCommand "github.com/scaleway/scaleway-cli/v2/internal/namespaces/init"
13+
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/login/webcallback"
14+
iam "github.com/scaleway/scaleway-sdk-go/api/iam/v1alpha1"
15+
"github.com/scaleway/scaleway-sdk-go/logger"
16+
"github.com/scaleway/scaleway-sdk-go/scw"
17+
"github.com/skratchdot/open-golang/open"
18+
)
19+
20+
func GetCommands() *core.Commands {
21+
return core.NewCommands(loginCommand())
22+
}
23+
24+
type loginArgs struct {
25+
Port int `json:"port"`
26+
// PrintURL will print the account url instead of trying to open it with a browser
27+
PrintURL bool `json:"print_url"`
28+
}
29+
30+
func loginCommand() *core.Command {
31+
return &core.Command{
32+
Groups: []string{"config"},
33+
Short: `Login to scaleway`,
34+
Long: `Start an interactive connection to scaleway to initialize the active profile of the config
35+
A webpage will open while the CLI will wait for a response.
36+
Once you connected to Scaleway, the profile should be configured.
37+
`,
38+
Namespace: "login",
39+
AllowAnonymousClient: true,
40+
ArgsType: reflect.TypeOf(loginArgs{}),
41+
ArgSpecs: core.ArgSpecs{
42+
{
43+
Name: "port",
44+
Short: "The port number used to wait for browser's response",
45+
},
46+
},
47+
SeeAlsos: []*core.SeeAlso{
48+
{
49+
Short: "Init profile manually",
50+
Command: "scw init",
51+
},
52+
},
53+
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
54+
args := argsI.(*loginArgs)
55+
56+
opts := []webcallback.Options(nil)
57+
if args.Port > 0 {
58+
opts = append(opts, webcallback.WithPort(args.Port))
59+
}
60+
61+
wb := webcallback.New(opts...)
62+
err := wb.Start()
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
callbackURL := fmt.Sprintf("http://localhost:%d/callback", wb.Port())
68+
69+
accountURL := "https://account.scaleway.com/authenticate?redirectToUrl=" + callbackURL
70+
71+
logger.Debugf("Web server started, waiting for callback on %s\n", callbackURL)
72+
73+
if args.PrintURL {
74+
fmt.Println(accountURL)
75+
} else {
76+
err = open.Start(accountURL)
77+
if err != nil {
78+
logger.Warningf("Failed to open web url, you may not have a default browser configured")
79+
logger.Warningf("You can open it: " + accountURL)
80+
}
81+
}
82+
83+
fmt.Println("waiting for callback from browser...")
84+
token, err := wb.Wait(ctx)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
rawToken, err := base64.StdEncoding.DecodeString(token)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
tt := Token{}
95+
96+
err = json.Unmarshal(rawToken, &tt)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
client, err := scw.NewClient(scw.WithJWT(tt.Token))
102+
if err != nil {
103+
return nil, err
104+
}
105+
106+
api := iam.NewAPI(client)
107+
apiKey, err := api.CreateAPIKey(&iam.CreateAPIKeyRequest{
108+
UserID: &tt.Jwt.AudienceID,
109+
Description: "Generated by the Scaleway CLI",
110+
})
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
resp, err := initCommand.Command().Run(ctx, &initCommand.Args{
116+
AccessKey: apiKey.AccessKey,
117+
SecretKey: *apiKey.SecretKey,
118+
ProjectID: apiKey.DefaultProjectID,
119+
OrganizationID: apiKey.DefaultProjectID,
120+
Region: scw.RegionFrPar,
121+
Zone: scw.ZoneFrPar1,
122+
})
123+
if err != nil {
124+
// Cleanup API Key if init failed
125+
logger.Warningf("Init failed, cleaning API key.\n")
126+
cleanErr := api.DeleteAPIKey(&iam.DeleteAPIKeyRequest{
127+
AccessKey: apiKey.AccessKey,
128+
})
129+
if cleanErr != nil {
130+
logger.Warningf("Failed to clean API key: %s\n", err.Error())
131+
}
132+
return nil, err
133+
}
134+
135+
return resp, nil
136+
},
137+
}
138+
}
139+
140+
type Token struct {
141+
Jwt struct {
142+
AudienceID string `json:"audienceId"`
143+
CreatedAt time.Time `json:"createdAt"`
144+
ExpiresAt time.Time `json:"expiresAt"`
145+
IP string `json:"ip"`
146+
IssuerID string `json:"issuerId"`
147+
Jti string `json:"jti"`
148+
UpdatedAt time.Time `json:"updatedAt"`
149+
UserAgent string `json:"userAgent"`
150+
} `json:"jwt"`
151+
RenewToken string `json:"renewToken"`
152+
Token string `json:"token"`
153+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package webcallback
2+
3+
type Options func(*WebCallback)
4+
5+
func WithPort(port int) Options {
6+
return func(callback *WebCallback) {
7+
callback.port = port
8+
}
9+
}

0 commit comments

Comments
 (0)