Skip to content

Commit 6b5499a

Browse files
committed
Merge pull request #107 from QuentinPerez/add_upload_ssh_key_at_login
upload ssh key
2 parents c928118 + 76f81e0 commit 6b5499a

File tree

3 files changed

+183
-13
lines changed

3 files changed

+183
-13
lines changed

pkg/api/api.go

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ type ScalewayServer struct {
400400
// StateDetail is the detailed status of the server
401401
StateDetail string `json:"state_detail,omitempty"`
402402

403-
// PrivateIP reprensents the private IPV4 attached to the server (changes on each boot)
403+
// PrivateIP represents the private IPV4 attached to the server (changes on each boot)
404404
PrivateIP string `json:"private_ip,omitempty"`
405405

406406
// Bootscript is the unique identifier of the selected bootscript
@@ -497,6 +497,37 @@ type ScalewayImageDefinition struct {
497497
Arch string `json:"arch"`
498498
}
499499

500+
// ScalewayTokenUserIDRoleDefinition represents a Scaleway Token UserId Role
501+
type ScalewayTokenUserIDRoleDefinition struct {
502+
Organization string `json:"organization,omitempty"`
503+
Role string `json:"role,omitempty"`
504+
}
505+
506+
// ScalewayTokenDefinition represents a Scaleway Token
507+
type ScalewayTokenDefinition struct {
508+
UserID string `json:"user_id"`
509+
Description string `json:"description,omitempty"`
510+
Roles ScalewayTokenUserIDRoleDefinition `json:"roles"`
511+
Expires string `json:"expires"`
512+
InheritsUsersPerms bool `json:"inherits_user_perms"`
513+
ID string `json:"id"`
514+
}
515+
516+
// ScalewayTokensDefinition represents a Scaleway Tokens
517+
type ScalewayTokensDefinition struct {
518+
Tokens []ScalewayTokenDefinition `json:"tokens"`
519+
}
520+
521+
// ScalewayUserPatchKeyDefinition represents a key
522+
type ScalewayUserPatchKeyDefinition struct {
523+
Key string `json:"key"`
524+
}
525+
526+
// ScalewayUserPatchDefinition represents a User Patch
527+
type ScalewayUserPatchDefinition struct {
528+
SSHPublicKeys []ScalewayUserPatchKeyDefinition `json:"ssh_public_keys"`
529+
}
530+
500531
// FuncMap used for json inspection
501532
var FuncMap = template.FuncMap{
502533
"json": func(v interface{}) string {
@@ -752,7 +783,7 @@ func (s *ScalewayAPI) DeleteServer(serverID string) error {
752783
func (s *ScalewayAPI) PostServer(definition ScalewayServerDefinition) (string, error) {
753784
definition.Organization = s.Organization
754785

755-
resp, err := s.PostResponse(fmt.Sprintf("servers"), definition)
786+
resp, err := s.PostResponse("servers", definition)
756787
if err != nil {
757788
return "", err
758789
}
@@ -783,6 +814,34 @@ func (s *ScalewayAPI) PostServer(definition ScalewayServerDefinition) (string, e
783814
return "", error
784815
}
785816

817+
// PatchUser updates a user
818+
func (s *ScalewayAPI) PatchUser(UserID string, definition ScalewayUserPatchDefinition) error {
819+
s.enableAccountAPI()
820+
defer s.disableAccountAPI()
821+
resp, err := s.PatchResponse(fmt.Sprintf("users/%s", UserID), definition)
822+
if err != nil {
823+
return err
824+
}
825+
826+
defer resp.Body.Close()
827+
decoder := json.NewDecoder(resp.Body)
828+
829+
// Succeed PATCH code
830+
if resp.StatusCode == 200 {
831+
return nil
832+
}
833+
834+
var error ScalewayAPIError
835+
err = decoder.Decode(&error)
836+
if err != nil {
837+
return err
838+
}
839+
840+
error.StatusCode = resp.StatusCode
841+
error.Debug()
842+
return error
843+
}
844+
786845
// PatchServer updates a server
787846
func (s *ScalewayAPI) PatchServer(serverID string, definition ScalewayServerPatchDefinition) error {
788847
resp, err := s.PatchResponse(fmt.Sprintf("servers/%s", serverID), definition)
@@ -894,7 +953,7 @@ func (s *ScalewayAPI) PostVolume(definition ScalewayVolumeDefinition) (string, e
894953
if definition.Type == "" {
895954
definition.Type = "l_ssd"
896955
}
897-
resp, err := s.PostResponse(fmt.Sprintf("volumes"), definition)
956+
resp, err := s.PostResponse("volumes", definition)
898957
if err != nil {
899958
return "", err
900959
}
@@ -1201,20 +1260,46 @@ func (s *ScalewayAPI) GetTasks() (*[]ScalewayTask, error) {
12011260

12021261
// CheckCredentials performs a dummy check to ensure we can contact the API
12031262
func (s *ScalewayAPI) CheckCredentials() error {
1204-
s.enableAccountApi()
1205-
defer s.disableAccountApi()
1263+
s.enableAccountAPI()
1264+
defer s.disableAccountAPI()
12061265
query := url.Values{}
12071266
query.Set("token_id", s.Token)
12081267
resp, err := s.GetResponse("tokens?" + query.Encode())
12091268
if err != nil {
12101269
return err
12111270
}
12121271
if resp.StatusCode != 200 {
1213-
return fmt.Errorf("invalid credentials")
1272+
return fmt.Errorf("[%d] invalid credentials", resp.StatusCode)
12141273
}
12151274
return nil
12161275
}
12171276

1277+
// GetUserID returns the UserID
1278+
func (s *ScalewayAPI) GetUserID() (string, error) {
1279+
s.enableAccountAPI()
1280+
defer s.disableAccountAPI()
1281+
resp, err := s.GetResponse("tokens")
1282+
if err != nil {
1283+
return "", err
1284+
}
1285+
if resp.StatusCode != 200 {
1286+
return "", fmt.Errorf("[%d] invalid credentials", resp.StatusCode)
1287+
}
1288+
defer resp.Body.Close()
1289+
var tokens ScalewayTokensDefinition
1290+
decoder := json.NewDecoder(resp.Body)
1291+
err = decoder.Decode(&tokens)
1292+
if err != nil {
1293+
return "", err
1294+
}
1295+
if len(tokens.Tokens) == 0 {
1296+
return "", fmt.Errorf("Unable to get tokens")
1297+
}
1298+
return tokens.Tokens[0].UserID, nil
1299+
}
1300+
1301+
//
1302+
12181303
// GetServerID returns exactly one server matching or dies
12191304
func (s *ScalewayAPI) GetServerID(needle string) string {
12201305
// Parses optional type prefix, i.e: "server:name" -> "name"
@@ -1322,10 +1407,10 @@ func (s *ScalewayAPI) HideAPICredentials(input string) string {
13221407
return output
13231408
}
13241409

1325-
func (s *ScalewayAPI) enableAccountApi() {
1410+
func (s *ScalewayAPI) enableAccountAPI() {
13261411
s.APIUrl = s.AccountAPI
13271412
}
13281413

1329-
func (s *ScalewayAPI) disableAccountApi() {
1414+
func (s *ScalewayAPI) disableAccountAPI() {
13301415
s.APIUrl = s.ComputeAPI
13311416
}

pkg/commands/login.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,75 @@ package commands
77
import (
88
"bufio"
99
"fmt"
10+
"io/ioutil"
1011
"os"
12+
"path/filepath"
13+
"strconv"
1114
"strings"
1215

1316
"github.com/scaleway/scaleway-cli/vendor/github.com/Sirupsen/logrus"
1417
"github.com/scaleway/scaleway-cli/vendor/golang.org/x/crypto/ssh/terminal"
1518

1619
"github.com/scaleway/scaleway-cli/pkg/api"
20+
"github.com/scaleway/scaleway-cli/pkg/utils"
1721
)
1822

1923
// LoginArgs are arguments passed to `RunLogin`
2024
type LoginArgs struct {
2125
Organization string
2226
Token string
27+
SSHKey string
28+
}
29+
30+
// selectKey allows to choice a key in ~/.ssh
31+
func selectKey(args *LoginArgs) error {
32+
fmt.Println("Do you want to upload a SSH key ?")
33+
home, err := utils.GetHomeDir()
34+
if err != nil {
35+
return err
36+
}
37+
dir := filepath.Join(home, ".ssh")
38+
files, err := ioutil.ReadDir(dir)
39+
if err != nil {
40+
return fmt.Errorf("Unable to open your ~/.ssh: %v", err)
41+
}
42+
var pubs []string
43+
44+
for i := range files {
45+
if filepath.Ext(files[i].Name()) == ".pub" {
46+
pubs = append(pubs, files[i].Name())
47+
}
48+
}
49+
if len(pubs) == 0 {
50+
return nil
51+
}
52+
fmt.Println("[0] I don't want to upload a key !")
53+
for i := range pubs {
54+
fmt.Printf("[%d] %s\n", i+1, pubs[i])
55+
}
56+
for {
57+
promptUser("Which [id]: ", &args.SSHKey, true)
58+
id, err := strconv.ParseUint(strings.TrimSpace(args.SSHKey), 10, 32)
59+
if err != nil {
60+
fmt.Println(err)
61+
continue
62+
}
63+
if int(id) > len(pubs) {
64+
fmt.Println("Out of range id must be lower than", len(pubs))
65+
continue
66+
}
67+
args.SSHKey = ""
68+
if id == 0 {
69+
break
70+
}
71+
buff, err := ioutil.ReadFile(filepath.Join(dir, pubs[id-1]))
72+
if err != nil {
73+
return fmt.Errorf("Unable to open your key: %v", err)
74+
}
75+
args.SSHKey = string(buff[:len(buff)])
76+
break
77+
}
78+
return nil
2379
}
2480

2581
// RunLogin is the handler for 'scw login'
@@ -39,14 +95,35 @@ func RunLogin(ctx CommandContext, args LoginArgs) error {
3995
Token: strings.Trim(args.Token, "\n"),
4096
}
4197

42-
api, err := api.NewScalewayAPI(cfg.ComputeAPI, cfg.AccountAPI, cfg.Organization, cfg.Token)
98+
apiConnection, err := api.NewScalewayAPI(cfg.ComputeAPI, cfg.AccountAPI, cfg.Organization, cfg.Token)
4399
if err != nil {
44100
return fmt.Errorf("Unable to create ScalewayAPI: %s", err)
45101
}
46-
err = api.CheckCredentials()
102+
err = apiConnection.CheckCredentials()
47103
if err != nil {
48104
return fmt.Errorf("Unable to contact ScalewayAPI: %s", err)
49105
}
106+
if err := selectKey(&args); err != nil {
107+
logrus.Errorf("Unable to select a key: %v", err)
108+
} else {
109+
if args.SSHKey != "" {
110+
userID, err := apiConnection.GetUserID()
111+
if err != nil {
112+
logrus.Errorf("Unable to contact ScalewayAPI: %s", err)
113+
} else {
114+
115+
SSHKey := api.ScalewayUserPatchDefinition{
116+
SSHPublicKeys: []api.ScalewayUserPatchKeyDefinition{{
117+
Key: strings.Trim(args.SSHKey, "\n"),
118+
}},
119+
}
120+
121+
if err = apiConnection.PatchUser(userID, SSHKey); err != nil {
122+
logrus.Errorf("Unable to patch SSHkey: %v", err)
123+
}
124+
}
125+
}
126+
}
50127
return cfg.Save()
51128
}
52129

pkg/utils/utils.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,25 @@ func RemoveDuplicates(elements []string) []string {
166166
return result
167167
}
168168

169-
// GetConfigFilePath returns the path to the Scaleway CLI config file
170-
func GetConfigFilePath() (string, error) {
169+
// GetHomeDir returns the path to your home
170+
func GetHomeDir() (string, error) {
171171
homeDir := os.Getenv("HOME") // *nix
172172
if homeDir == "" { // Windows
173173
homeDir = os.Getenv("USERPROFILE")
174174
}
175175
if homeDir == "" {
176176
return "", errors.New("user home directory not found")
177177
}
178+
return homeDir, nil
179+
}
178180

179-
return filepath.Join(homeDir, ".scwrc"), nil
181+
// GetConfigFilePath returns the path to the Scaleway CLI config file
182+
func GetConfigFilePath() (string, error) {
183+
path, err := GetHomeDir()
184+
if err != nil {
185+
return "", err
186+
}
187+
return filepath.Join(path, ".scwrc"), nil
180188
}
181189

182190
const termjsBin string = "termjs-cli"

0 commit comments

Comments
 (0)