Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading