Skip to content

Commit 49d163f

Browse files
authored
refactor: init command (#3089)
1 parent 859d891 commit 49d163f

18 files changed

+669
-492
lines changed

internal/core/validate.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ func stringExists(strs []string, s string) bool {
176176
func ValidateSecretKey() ArgSpecValidateFunc {
177177
return func(argSpec *ArgSpec, valueI interface{}) error {
178178
value := valueI.(string)
179+
if value == "" && !argSpec.Required {
180+
return nil
181+
}
179182
err := DefaultArgSpecValidateFunc()(argSpec, value)
180183
if err != nil {
181184
return err
@@ -191,6 +194,9 @@ func ValidateSecretKey() ArgSpecValidateFunc {
191194
func ValidateAccessKey() ArgSpecValidateFunc {
192195
return func(argSpec *ArgSpec, valueI interface{}) error {
193196
value := valueI.(string)
197+
if value == "" && !argSpec.Required {
198+
return nil
199+
}
194200
err := DefaultArgSpecValidateFunc()(argSpec, value)
195201
if err != nil {
196202
return err

internal/namespaces/init/init.go

Lines changed: 69 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ import (
1515
iamcommands "github.com/scaleway/scaleway-cli/v2/internal/namespaces/iam/v1alpha1"
1616
"github.com/scaleway/scaleway-cli/v2/internal/terminal"
1717
iam "github.com/scaleway/scaleway-sdk-go/api/iam/v1alpha1"
18-
"github.com/scaleway/scaleway-sdk-go/logger"
1918
"github.com/scaleway/scaleway-sdk-go/scw"
20-
"github.com/scaleway/scaleway-sdk-go/validation"
2119
)
2220

2321
/*
@@ -129,191 +127,80 @@ Default path for configuration file is based on the following priority order:
129127
Command: "scw config --help",
130128
},
131129
},
132-
PreValidateFunc: func(ctx context.Context, argsI interface{}) error {
130+
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
133131
args := argsI.(*initArgs)
134132

133+
profileName := core.ExtractProfileName(ctx)
134+
configPath := core.ExtractConfigPath(ctx)
135+
135136
// Show logo banner, or simple welcome message
136-
if terminal.GetWidth() >= 80 {
137-
interactive.Printf("%s\n%s\n\n", interactive.Center(logo), interactive.Line("-"))
138-
} else {
139-
interactive.Printf("Welcome to the Scaleway Cli\n\n")
140-
}
137+
printScalewayBanner()
141138

142-
config, err := scw.LoadConfigFromPath(core.ExtractConfigPath(ctx))
143-
144-
// If it is not a new config, ask if we want to override the existing config
145-
if err == nil && !config.IsEmpty() {
146-
_, _ = interactive.PrintlnWithoutIndent(`
147-
Current config is located at ` + core.ExtractConfigPath(ctx) + `
148-
` + terminal.Style(fmt.Sprint(config), color.Faint) + `
149-
`)
150-
overrideConfig, err := interactive.PromptBoolWithConfig(&interactive.PromptBoolConfig{
151-
Prompt: "Do you want to override the current config?",
152-
DefaultValue: true,
153-
Ctx: ctx,
154-
})
155-
if err != nil {
156-
return err
157-
}
158-
if !overrideConfig {
159-
return fmt.Errorf("initialization canceled")
160-
}
139+
err := promptProfileOverride(ctx, configPath, profileName)
140+
if err != nil {
141+
return nil, err
161142
}
162143

163-
// Manually prompt for missing args:
164-
165144
// Credentials
166145
if args.SecretKey == "" {
167-
_, _ = interactive.Println()
168-
args.SecretKey, err = promptSecret(ctx)
146+
args.SecretKey, err = promptSecretKey(ctx)
169147
if err != nil {
170-
return err
148+
return nil, err
171149
}
172150
}
173151

174152
if args.AccessKey == "" {
175-
_, _ = interactive.Println()
176153
args.AccessKey, err = promptAccessKey(ctx)
177154
if err != nil {
178-
return err
155+
return nil, err
179156
}
180157
}
181158

182159
if args.OrganizationID == "" {
183-
_, _ = interactive.Println()
184-
args.OrganizationID, err = interactive.PromptStringWithConfig(&interactive.PromptStringConfig{
185-
Ctx: ctx,
186-
Prompt: "Choose your default organization ID",
187-
ValidateFunc: func(s string) error {
188-
if !validation.IsUUID(s) {
189-
return fmt.Errorf("organization id is not a valid uuid")
190-
}
191-
return nil
192-
},
193-
})
160+
args.OrganizationID, err = promptOrganizationID(ctx)
194161
if err != nil {
195-
return err
162+
return nil, err
196163
}
197164
}
198165

199-
// Zone
200-
if args.Zone == "" {
201-
_, _ = interactive.Println()
202-
zone, err := interactive.PromptStringWithConfig(&interactive.PromptStringConfig{
203-
Ctx: ctx,
204-
Prompt: "Select a zone",
205-
DefaultValueDoc: "fr-par-1",
206-
DefaultValue: "fr-par-1",
207-
ValidateFunc: func(s string) error {
208-
logger.Debugf("s: %v", s)
209-
if !validation.IsZone(s) {
210-
return fmt.Errorf("invalid zone")
211-
}
212-
return nil
213-
},
214-
})
166+
if args.ProjectID == "" {
167+
args.ProjectID = getAPIKeyDefaultProjectID(ctx, args.AccessKey, args.SecretKey)
168+
}
169+
170+
if args.ProjectID == "" {
171+
args.ProjectID, err = promptProjectID(ctx)
215172
if err != nil {
216-
return err
173+
return nil, err
217174
}
218-
args.Zone, err = scw.ParseZone(zone)
175+
}
176+
177+
// Ask for default zone, currently not used as CLI will default to fr-par-1
178+
if args.Zone == "" {
179+
args.Zone, err = promptDefaultZone(ctx)
219180
if err != nil {
220-
return err
181+
return nil, err
221182
}
222183
}
223184

224185
// Deduce Region from Zone
225186
if args.Region == "" {
226187
args.Region, err = args.Zone.Region()
227188
if err != nil {
228-
return err
189+
return nil, err
229190
}
230191
}
231192

232193
// Ask for send usage permission
233194
if args.SendTelemetry == nil {
234-
_, _ = interactive.Println()
235-
_, _ = interactive.PrintlnWithoutIndent(`
236-
To improve this tool we rely on diagnostic and usage data.
237-
Sending such data is optional and can be disabled at any time by running "scw config set send-telemetry=false".
238-
`)
239-
240-
sendTelemetry, err := interactive.PromptBoolWithConfig(&interactive.PromptBoolConfig{
241-
Prompt: "Do you want to send usage statistics and diagnostics?",
242-
DefaultValue: true,
243-
Ctx: ctx,
244-
})
195+
args.SendTelemetry, err = promptTelemetry(ctx)
245196
if err != nil {
246-
return err
197+
return nil, err
247198
}
248-
249-
args.SendTelemetry = scw.BoolPtr(sendTelemetry)
250199
}
251200

252201
// Ask whether we should install autocomplete
253202
if args.InstallAutocomplete == nil {
254-
_, _ = interactive.Println()
255-
_, _ = interactive.PrintlnWithoutIndent(`
256-
To fully enjoy Scaleway CLI we recommend you install autocomplete support in your shell.
257-
`)
258-
259-
installAutocomplete, err := interactive.PromptBoolWithConfig(&interactive.PromptBoolConfig{
260-
Ctx: ctx,
261-
Prompt: "Do you want to install autocomplete?",
262-
DefaultValue: true,
263-
})
264-
if err != nil {
265-
return err
266-
}
267-
268-
args.InstallAutocomplete = scw.BoolPtr(installAutocomplete)
269-
}
270-
271-
return nil
272-
},
273-
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
274-
args := argsI.(*initArgs)
275-
// Check if a config exists
276-
// Creates a new one if it does not
277-
configPath := core.ExtractConfigPath(ctx)
278-
config, err := scw.LoadConfigFromPath(configPath)
279-
if err != nil {
280-
_, ok := err.(*scw.ConfigFileNotFoundError)
281-
if ok {
282-
config = &scw.Config{}
283-
interactive.Printf("Creating new config at %s\n", configPath)
284-
} else {
285-
return nil, err
286-
}
287-
}
288-
289-
if args.SendTelemetry != nil {
290-
config.SendTelemetry = args.SendTelemetry
291-
}
292-
293-
client := core.ExtractClient(ctx)
294-
api := iam.NewAPI(client)
295-
296-
apiKey, err := api.GetAPIKey(&iam.GetAPIKeyRequest{AccessKey: args.AccessKey}, scw.WithAuthRequest(args.AccessKey, args.SecretKey))
297-
if err != nil && !is403Error(err) {
298-
// If 403 Unauthorized, API Key does not have permissions to get himself
299-
return nil, err
300-
}
301-
302-
if apiKey != nil && args.ProjectID == "" {
303-
args.ProjectID = apiKey.DefaultProjectID
304-
}
305-
306-
if args.ProjectID == "" {
307-
args.ProjectID, err = interactive.PromptStringWithConfig(&interactive.PromptStringConfig{
308-
Ctx: ctx,
309-
Prompt: "Default project ID",
310-
ValidateFunc: func(s string) error {
311-
if !validation.IsUUID(s) {
312-
return fmt.Errorf("given project ID is not a valid UUID")
313-
}
314-
return nil
315-
},
316-
})
203+
args.InstallAutocomplete, err = promptAutocomplete(ctx)
317204
if err != nil {
318205
return nil, err
319206
}
@@ -328,8 +215,12 @@ Default path for configuration file is based on the following priority order:
328215
DefaultProjectID: &args.ProjectID, // An API key is always bound to a project.
329216
}
330217

218+
config, err := loadConfigOrEmpty(configPath)
219+
if err != nil {
220+
return nil, err
221+
}
222+
331223
// Save the profile as default or as a named profile
332-
profileName := core.ExtractProfileName(ctx)
333224
if profileName == scw.DefaultProfileName {
334225
// Default configuration
335226
config.Profile = *profile
@@ -382,67 +273,48 @@ Default path for configuration file is based on the following priority order:
382273
}
383274
}
384275

385-
func promptSecret(ctx context.Context) (string, error) {
386-
secret, err := interactive.Readline(&interactive.ReadlineConfig{
387-
Ctx: ctx,
388-
PromptFunc: func(value string) string {
389-
secretKey := "secret-key"
390-
switch {
391-
case validation.IsUUID(value):
392-
secretKey = terminal.Style(secretKey, color.FgBlue)
393-
}
394-
return terminal.Style(fmt.Sprintf("Enter a valid %s: ", secretKey), color.Bold)
395-
},
396-
ValidateFunc: func(s string) error {
397-
if validation.IsSecretKey(s) {
398-
return nil
399-
}
400-
return fmt.Errorf("invalid secret-key")
401-
},
402-
})
403-
if err != nil {
404-
return "", err
405-
}
406-
407-
switch {
408-
case validation.IsUUID(secret):
409-
return secret, nil
410-
411-
default:
412-
return "", fmt.Errorf("invalid secret-key: '%v'", secret)
276+
func printScalewayBanner() {
277+
if terminal.GetWidth() >= 80 {
278+
interactive.Printf("%s\n%s\n\n", interactive.Center(logo), interactive.Line("-"))
279+
} else {
280+
interactive.Printf("Welcome to the Scaleway Cli\n\n")
413281
}
414282
}
415283

416-
func promptAccessKey(ctx context.Context) (string, error) {
417-
key, err := interactive.Readline(&interactive.ReadlineConfig{
418-
Ctx: ctx,
419-
PromptFunc: func(value string) string {
420-
accessKey := "access-key"
421-
switch {
422-
case validation.IsAccessKey(value):
423-
accessKey = terminal.Style(accessKey, color.FgBlue)
424-
}
425-
return terminal.Style(fmt.Sprintf("Enter a valid %s: ", accessKey), color.Bold)
426-
},
427-
ValidateFunc: func(s string) error {
428-
if !validation.IsAccessKey(s) {
429-
return fmt.Errorf("invalid access-key")
430-
}
431-
432-
return nil
433-
},
434-
})
284+
// loadConfigOrEmpty checks if a config exists
285+
// Creates a new one if it does not
286+
func loadConfigOrEmpty(configPath string) (*scw.Config, error) {
287+
config, err := scw.LoadConfigFromPath(configPath)
435288
if err != nil {
436-
return "", err
289+
_, ok := err.(*scw.ConfigFileNotFoundError)
290+
if ok {
291+
config = &scw.Config{}
292+
interactive.Printf("Creating new config\n")
293+
} else {
294+
return nil, err
295+
}
437296
}
297+
return config, nil
298+
}
438299

439-
switch {
440-
case validation.IsAccessKey(key):
441-
return key, nil
300+
// getAPIKeyDefaultProjectID tries to find the api-key default project ID
301+
// return an empty string if it cannot find it
302+
func getAPIKeyDefaultProjectID(ctx context.Context, accessKey string, secretKey string) string {
303+
client := core.ExtractClient(ctx)
304+
api := iam.NewAPI(client)
305+
306+
apiKey, err := api.GetAPIKey(&iam.GetAPIKeyRequest{AccessKey: accessKey}, scw.WithAuthRequest(accessKey, secretKey))
307+
if err != nil && !is403Error(err) {
308+
// If 403 Unauthorized, API Key does not have permissions to get himself
309+
// It requires IAM permission to fetch an API Key
310+
return ""
311+
}
442312

443-
default:
444-
return "", fmt.Errorf("invalid access-key: '%v'", key)
313+
if apiKey == nil {
314+
return ""
445315
}
316+
317+
return apiKey.DefaultProjectID
446318
}
447319

448320
// isHTTPCodeError returns true if err is an http error with code statusCode

0 commit comments

Comments
 (0)