Skip to content

Commit

Permalink
feat: add login command (scaleway#4043)
Browse files Browse the repository at this point in the history
  • Loading branch information
Codelax authored Aug 27, 2024
1 parent c5f01ed commit 642b368
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 7 deletions.
24 changes: 24 additions & 0 deletions cmd/scw/testdata/test-all-usage-login-usage.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
Start an interactive connection to scaleway to initialize the active profile of the config
A webpage will open while the CLI will wait for a response.
Once you connected to Scaleway, the profile should be configured.

USAGE:
scw login [arg=value ...]

ARGS:
[port] The port number used to wait for browser's response

FLAGS:
-h, --help help for login

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use

SEE ALSO:
# Init profile manually
scw init
1 change: 1 addition & 0 deletions cmd/scw/testdata/test-main-usage-usage.golden
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ CONFIGURATION COMMANDS:
config Config file management
info Get info about current settings
init Initialize the config
login Login to scaleway

UTILITY COMMANDS:
feedback Send feedback to the Scaleway CLI Team!
Expand Down
9 changes: 9 additions & 0 deletions docs/commands/login.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- DO NOT EDIT: this file is automatically generated using scw-doc-gen -->
# Documentation for `scw login`
Start an interactive connection to scaleway to initialize the active profile of the config
A webpage will open while the CLI will wait for a response.
Once you connected to Scaleway, the profile should be configured.




2 changes: 2 additions & 0 deletions internal/namespaces/get_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/k8s/v1"
keymanager "github.com/scaleway/scaleway-cli/v2/internal/namespaces/key_manager/v1alpha1"
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/lb/v1"
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/login"
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/marketplace/v2"
mnq "github.com/scaleway/scaleway-cli/v2/internal/namespaces/mnq/v1beta1"
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/object/v1"
Expand Down Expand Up @@ -104,6 +105,7 @@ func GetCommands() *core.Commands {
jobs.GetCommands(),
serverless_sqldb.GetCommands(),
edgeservices.GetCommands(),
login.GetCommands(),
)

if beta {
Expand Down
18 changes: 11 additions & 7 deletions internal/namespaces/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ See below the schema `scw init` follows to ask for default config:
*/

func GetCommands() *core.Commands {
return core.NewCommands(initCommand())
return core.NewCommands(Command())
}

type initArgs struct {
type Args struct {
AccessKey string
SecretKey string
ProjectID string
Expand All @@ -70,7 +70,7 @@ type initArgs struct {
InstallAutocomplete *bool
}

func initCommand() *core.Command {
func Command() *core.Command {
return &core.Command{
Groups: []string{"config"},
Short: `Initialize the config`,
Expand All @@ -83,7 +83,7 @@ Default path for configuration file is based on the following priority order:
- $USERPROFILE/.config/scw/config.yaml`,
Namespace: "init",
AllowAnonymousClient: true,
ArgsType: reflect.TypeOf(initArgs{}),
ArgsType: reflect.TypeOf(Args{}),
ArgSpecs: core.ArgSpecs{
{
Name: "secret-key",
Expand Down Expand Up @@ -126,9 +126,13 @@ Default path for configuration file is based on the following priority order:
Short: "Config management help",
Command: "scw config",
},
{
Short: "Login through a web page",
Command: "scw login",
},
},
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
args := argsI.(*initArgs)
args := argsI.(*Args)

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

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

// Init SSH Key
if *args.WithSSHKey {
if args.WithSSHKey != nil && *args.WithSSHKey {
_, _ = interactive.Println()
_, err := iamcommands.InitWithSSHKeyRun(ctx, nil)
if err != nil {
Expand Down
153 changes: 153 additions & 0 deletions internal/namespaces/login/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package login

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
"time"

"github.com/scaleway/scaleway-cli/v2/internal/core"
initCommand "github.com/scaleway/scaleway-cli/v2/internal/namespaces/init"
"github.com/scaleway/scaleway-cli/v2/internal/namespaces/login/webcallback"
iam "github.com/scaleway/scaleway-sdk-go/api/iam/v1alpha1"
"github.com/scaleway/scaleway-sdk-go/logger"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/skratchdot/open-golang/open"
)

func GetCommands() *core.Commands {
return core.NewCommands(loginCommand())
}

type loginArgs struct {
Port int `json:"port"`
// PrintURL will print the account url instead of trying to open it with a browser
PrintURL bool `json:"print_url"`
}

func loginCommand() *core.Command {
return &core.Command{
Groups: []string{"config"},
Short: `Login to scaleway`,
Long: `Start an interactive connection to scaleway to initialize the active profile of the config
A webpage will open while the CLI will wait for a response.
Once you connected to Scaleway, the profile should be configured.
`,
Namespace: "login",
AllowAnonymousClient: true,
ArgsType: reflect.TypeOf(loginArgs{}),
ArgSpecs: core.ArgSpecs{
{
Name: "port",
Short: "The port number used to wait for browser's response",
},
},
SeeAlsos: []*core.SeeAlso{
{
Short: "Init profile manually",
Command: "scw init",
},
},
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
args := argsI.(*loginArgs)

opts := []webcallback.Options(nil)
if args.Port > 0 {
opts = append(opts, webcallback.WithPort(args.Port))
}

wb := webcallback.New(opts...)
err := wb.Start()
if err != nil {
return nil, err
}

callbackURL := fmt.Sprintf("http://localhost:%d/callback", wb.Port())

accountURL := "https://account.scaleway.com/authenticate?redirectToUrl=" + callbackURL

logger.Debugf("Web server started, waiting for callback on %s\n", callbackURL)

if args.PrintURL {
fmt.Println(accountURL)
} else {
err = open.Start(accountURL)
if err != nil {
logger.Warningf("Failed to open web url, you may not have a default browser configured")
logger.Warningf("You can open it: " + accountURL)
}
}

fmt.Println("waiting for callback from browser...")
token, err := wb.Wait(ctx)
if err != nil {
return nil, err
}

rawToken, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, err
}

tt := Token{}

err = json.Unmarshal(rawToken, &tt)
if err != nil {
return nil, err
}

client, err := scw.NewClient(scw.WithJWT(tt.Token))
if err != nil {
return nil, err
}

api := iam.NewAPI(client)
apiKey, err := api.CreateAPIKey(&iam.CreateAPIKeyRequest{
UserID: &tt.Jwt.AudienceID,
Description: "Generated by the Scaleway CLI",
})
if err != nil {
return nil, err
}

resp, err := initCommand.Command().Run(ctx, &initCommand.Args{
AccessKey: apiKey.AccessKey,
SecretKey: *apiKey.SecretKey,
ProjectID: apiKey.DefaultProjectID,
OrganizationID: apiKey.DefaultProjectID,
Region: scw.RegionFrPar,
Zone: scw.ZoneFrPar1,
})
if err != nil {
// Cleanup API Key if init failed
logger.Warningf("Init failed, cleaning API key.\n")
cleanErr := api.DeleteAPIKey(&iam.DeleteAPIKeyRequest{
AccessKey: apiKey.AccessKey,
})
if cleanErr != nil {
logger.Warningf("Failed to clean API key: %s\n", err.Error())
}
return nil, err
}

return resp, nil
},
}
}

type Token struct {
Jwt struct {
AudienceID string `json:"audienceId"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
IP string `json:"ip"`
IssuerID string `json:"issuerId"`
Jti string `json:"jti"`
UpdatedAt time.Time `json:"updatedAt"`
UserAgent string `json:"userAgent"`
} `json:"jwt"`
RenewToken string `json:"renewToken"`
Token string `json:"token"`
}
9 changes: 9 additions & 0 deletions internal/namespaces/login/webcallback/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package webcallback

type Options func(*WebCallback)

func WithPort(port int) Options {
return func(callback *WebCallback) {
callback.port = port
}
}
Loading

0 comments on commit 642b368

Please sign in to comment.