Skip to content

Commit e44ab83

Browse files
yfodilremyleone
andauthored
feat(config): add validate command (#3018)
Co-authored-by: Rémy Léone <rleone@scaleway.com>
1 parent 9d076c3 commit e44ab83

File tree

8 files changed

+339
-1
lines changed

8 files changed

+339
-1
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
This command validates the configuration of your Scaleway CLI tool.
4+
5+
It performs the following checks:
6+
7+
- YAML syntax correctness: It checks whether your config file is a valid YAML file.
8+
- Field validity: It checks whether the fields present in the config file are valid and expected fields. This includes fields like AccessKey, SecretKey, DefaultOrganizationID, DefaultProjectID, DefaultRegion, DefaultZone, and APIURL.
9+
- Field values: For each of the fields mentioned above, it checks whether the value assigned to it is valid. For example, it checks if the AccessKey and SecretKey are non-empty and meet the format expectations.
10+
11+
The command goes through each profile present in the config file and validates it.
12+
13+
USAGE:
14+
scw config validate
15+
16+
FLAGS:
17+
-h, --help help for validate
18+
19+
GLOBAL FLAGS:
20+
-c, --config string The path to the config file
21+
-D, --debug Enable debug mode
22+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
23+
-p, --profile string The config profile to use

docs/commands/config.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Read more about the config management engine at https://github.com/scaleway/scal
3737
- [Reset the config](#reset-the-config)
3838
- [Set a line from the config file](#set-a-line-from-the-config-file)
3939
- [Unset a line from the config file](#unset-a-line-from-the-config-file)
40+
- [Validate the config](#validate-the-config)
4041

4142

4243
## Destroy the config file
@@ -277,3 +278,33 @@ scw config unset <key ...> [arg=value ...]
277278

278279

279280

281+
## Validate the config
282+
283+
This command validates the configuration of your Scaleway CLI tool.
284+
285+
It performs the following checks:
286+
287+
- YAML syntax correctness: It checks whether your config file is a valid YAML file.
288+
- Field validity: It checks whether the fields present in the config file are valid and expected fields. This includes fields like AccessKey, SecretKey, DefaultOrganizationID, DefaultProjectID, DefaultRegion, DefaultZone, and APIURL.
289+
- Field values: For each of the fields mentioned above, it checks whether the value assigned to it is valid. For example, it checks if the AccessKey and SecretKey are non-empty and meet the format expectations.
290+
291+
The command goes through each profile present in the config file and validates it.
292+
293+
This command validates the configuration of your Scaleway CLI tool.
294+
295+
It performs the following checks:
296+
297+
- YAML syntax correctness: It checks whether your config file is a valid YAML file.
298+
- Field validity: It checks whether the fields present in the config file are valid and expected fields. This includes fields like AccessKey, SecretKey, DefaultOrganizationID, DefaultProjectID, DefaultRegion, DefaultZone, and APIURL.
299+
- Field values: For each of the fields mentioned above, it checks whether the value assigned to it is valid. For example, it checks if the AccessKey and SecretKey are non-empty and meet the format expectations.
300+
301+
The command goes through each profile present in the config file and validates it.
302+
303+
**Usage:**
304+
305+
```
306+
scw config validate
307+
```
308+
309+
310+

internal/core/errors.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func InvalidSecretKeyError(value string) *CliError {
2626
func InvalidAccessKeyError(value string) *CliError {
2727
return &CliError{
2828
Err: fmt.Errorf("invalid access_key '%v'", value),
29-
Hint: "access_key should look like : SCWXXXXXXXXXXXXXXXXX.",
29+
Hint: "access_key should look like: SCWXXXXXXXXXXXXXXXXX.",
3030
}
3131
}
3232

@@ -44,6 +44,27 @@ func InvalidProjectIDError(value string) *CliError {
4444
}
4545
}
4646

47+
func InvalidRegionError(value string) *CliError {
48+
return &CliError{
49+
Err: fmt.Errorf("invalid region '%v'", value),
50+
Hint: "region format should look like: XX-XXX (e.g. fr-par).",
51+
}
52+
}
53+
54+
func InvalidZoneError(value string) *CliError {
55+
return &CliError{
56+
Err: fmt.Errorf("invalid zone '%v'", value),
57+
Hint: "zone format should look like XX-XXX-X: (e.g. fr-par-1).",
58+
}
59+
}
60+
61+
func InvalidAPIURLError(value string) *CliError {
62+
return &CliError{
63+
Err: fmt.Errorf("invalid api_url '%v'", value),
64+
Hint: "api_url should look like: https://www.example.com (e.g. https://api.scaleway.com).",
65+
}
66+
}
67+
4768
func ArgumentConflictError(arg1 string, arg2 string) *CliError {
4869
return &CliError{
4970
Err: fmt.Errorf("only one of those two arguments '%s' and '%s' can be specified in the same time", arg1, arg2),

internal/namespaces/config/commands.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func GetCommands() *core.Commands {
3333
configDestroyCommand(),
3434
configInfoCommand(),
3535
configImportCommand(),
36+
configValidateCommand(),
3637
)
3738
}
3839

@@ -695,6 +696,52 @@ func configImportCommand() *core.Command {
695696
}
696697
}
697698

699+
// configValidateCommand validates the config
700+
func configValidateCommand() *core.Command {
701+
type configValidateArgs struct{}
702+
703+
return &core.Command{
704+
Short: `Validate the config`,
705+
Long: `This command validates the configuration of your Scaleway CLI tool.
706+
707+
It performs the following checks:
708+
709+
- YAML syntax correctness: It checks whether your config file is a valid YAML file.
710+
- Field validity: It checks whether the fields present in the config file are valid and expected fields. This includes fields like AccessKey, SecretKey, DefaultOrganizationID, DefaultProjectID, DefaultRegion, DefaultZone, and APIURL.
711+
- Field values: For each of the fields mentioned above, it checks whether the value assigned to it is valid. For example, it checks if the AccessKey and SecretKey are non-empty and meet the format expectations.
712+
713+
The command goes through each profile present in the config file and validates it.`,
714+
Namespace: "config",
715+
Resource: "validate",
716+
AllowAnonymousClient: true,
717+
ArgsType: reflect.TypeOf(configValidateArgs{}),
718+
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
719+
configPath := core.ExtractConfigPath(ctx)
720+
config, err := scw.LoadConfigFromPath(configPath)
721+
if err != nil {
722+
return nil, err
723+
}
724+
725+
// validate default profile
726+
err = validateProfile(&config.Profile)
727+
if err != nil {
728+
return nil, err
729+
}
730+
// validate the remaining profiles
731+
for _, profile := range config.Profiles {
732+
err = validateProfile(profile)
733+
if err != nil {
734+
return nil, err
735+
}
736+
}
737+
738+
return &core.SuccessResult{
739+
Message: "successfully validate config",
740+
}, nil
741+
},
742+
}
743+
}
744+
698745
// Helper functions
699746
func getProfileValue(profile *scw.Profile, fieldName string) (interface{}, error) {
700747
field, err := getProfileField(profile, fieldName)
@@ -749,3 +796,124 @@ func getProfile(config *scw.Config, profileName string) (*scw.Profile, error) {
749796
}
750797
return profile, nil
751798
}
799+
800+
func validateProfile(profile *scw.Profile) error {
801+
if err := validateAccessKey(profile); err != nil {
802+
return err
803+
}
804+
if err := validateSecretKey(profile); err != nil {
805+
return err
806+
}
807+
if err := validateDefaultOrganizationID(profile); err != nil {
808+
return err
809+
}
810+
if err := validateDefaultProjectID(profile); err != nil {
811+
return err
812+
}
813+
if err := validateDefaultRegion(profile); err != nil {
814+
return err
815+
}
816+
if err := validateDefaultZone(profile); err != nil {
817+
return err
818+
}
819+
return validateAPIURL(profile)
820+
}
821+
822+
func validateAccessKey(profile *scw.Profile) error {
823+
if profile.AccessKey != nil {
824+
if *profile.AccessKey == "" {
825+
return &core.CliError{
826+
Err: fmt.Errorf("access key cannot be empty"),
827+
}
828+
}
829+
830+
if !validation.IsAccessKey(*profile.AccessKey) {
831+
return core.InvalidAccessKeyError(*profile.AccessKey)
832+
}
833+
}
834+
return nil
835+
}
836+
837+
func validateSecretKey(profile *scw.Profile) error {
838+
if profile.SecretKey != nil {
839+
if *profile.SecretKey == "" {
840+
return &core.CliError{
841+
Err: fmt.Errorf("secret key cannot be empty"),
842+
}
843+
}
844+
845+
if !validation.IsSecretKey(*profile.SecretKey) {
846+
return core.InvalidSecretKeyError(*profile.SecretKey)
847+
}
848+
}
849+
return nil
850+
}
851+
852+
func validateDefaultOrganizationID(profile *scw.Profile) error {
853+
if profile.DefaultOrganizationID != nil {
854+
if *profile.DefaultOrganizationID == "" {
855+
return &core.CliError{
856+
Err: fmt.Errorf("default organization ID cannot be empty"),
857+
}
858+
}
859+
860+
if !validation.IsOrganizationID(*profile.DefaultOrganizationID) {
861+
return core.InvalidOrganizationIDError(*profile.DefaultOrganizationID)
862+
}
863+
}
864+
return nil
865+
}
866+
867+
func validateDefaultProjectID(profile *scw.Profile) error {
868+
if profile.DefaultProjectID != nil {
869+
if *profile.DefaultProjectID == "" {
870+
return &core.CliError{
871+
Err: fmt.Errorf("default project ID cannot be empty"),
872+
}
873+
}
874+
875+
if !validation.IsProjectID(*profile.DefaultProjectID) {
876+
return core.InvalidProjectIDError(*profile.DefaultProjectID)
877+
}
878+
}
879+
return nil
880+
}
881+
882+
func validateDefaultRegion(profile *scw.Profile) error {
883+
if profile.DefaultRegion != nil {
884+
if *profile.DefaultRegion == "" {
885+
return &core.CliError{
886+
Err: fmt.Errorf("default region cannot be empty"),
887+
}
888+
}
889+
890+
if !validation.IsRegion(*profile.DefaultRegion) {
891+
return core.InvalidRegionError(*profile.DefaultRegion)
892+
}
893+
}
894+
return nil
895+
}
896+
897+
func validateDefaultZone(profile *scw.Profile) error {
898+
if profile.DefaultZone != nil {
899+
if *profile.DefaultZone == "" {
900+
return &core.CliError{
901+
Err: fmt.Errorf("default zone cannot be empty"),
902+
}
903+
}
904+
905+
if !validation.IsZone(*profile.DefaultZone) {
906+
return core.InvalidZoneError(*profile.DefaultZone)
907+
}
908+
}
909+
return nil
910+
}
911+
912+
func validateAPIURL(profile *scw.Profile) error {
913+
if profile.APIURL != nil {
914+
if *profile.APIURL != "" && !validation.IsURL(*profile.APIURL) {
915+
return core.InvalidAPIURLError(*profile.APIURL)
916+
}
917+
}
918+
return nil
919+
}

internal/namespaces/config/commands_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,42 @@ func Test_ConfigImportCommand(t *testing.T) {
336336
})
337337
}
338338

339+
func Test_ConfigValidateCommand(t *testing.T) {
340+
t.Run("Simple", core.Test(&core.TestConfig{
341+
Commands: GetCommands(),
342+
BeforeFunc: beforeFuncCreateFullConfig(),
343+
Cmd: "scw config validate",
344+
Check: core.TestCheckCombine(
345+
core.TestCheckExitCode(0),
346+
core.TestCheckGolden(),
347+
),
348+
TmpHomeDir: true,
349+
}))
350+
t.Run("Invalid default access key", core.Test(&core.TestConfig{
351+
Commands: GetCommands(),
352+
BeforeFunc: beforeFuncCreateInvalidConfig(),
353+
Cmd: "scw config validate",
354+
Check: core.TestCheckCombine(
355+
core.TestCheckExitCode(1),
356+
core.TestCheckGolden(),
357+
),
358+
TmpHomeDir: true,
359+
}))
360+
t.Run("Invalid profile p1 secret key", core.Test(&core.TestConfig{
361+
Commands: GetCommands(),
362+
BeforeFunc: core.BeforeFuncCombine(
363+
beforeFuncCreateInvalidConfig(),
364+
core.ExecBeforeCmd("scw config set access-key=SCWNEWXXXXXXXXXXXXXX"),
365+
),
366+
Cmd: "scw config validate",
367+
Check: core.TestCheckCombine(
368+
core.TestCheckExitCode(1),
369+
core.TestCheckGolden(),
370+
),
371+
TmpHomeDir: true,
372+
}))
373+
}
374+
339375
func checkConfig(f func(t *testing.T, config *scw.Config)) core.TestCheck {
340376
return func(t *testing.T, ctx *core.CheckFuncCtx) {
341377
homeDir := ctx.OverrideEnv["HOME"]
@@ -395,6 +431,33 @@ func beforeFuncCreateFullConfig() core.BeforeFunc {
395431
})
396432
}
397433

434+
func beforeFuncCreateInvalidConfig() core.BeforeFunc {
435+
return beforeFuncCreateConfigFile(&scw.Config{
436+
Profile: scw.Profile{
437+
AccessKey: scw.StringPtr("invalidAccessKey"),
438+
SecretKey: scw.StringPtr("11111111-1111-1111-1111-111111111111"),
439+
APIURL: scw.StringPtr("https://mock-api-url.com"),
440+
Insecure: scw.BoolPtr(true),
441+
DefaultOrganizationID: scw.StringPtr("11111111-1111-1111-1111-111111111111"),
442+
DefaultRegion: scw.StringPtr("fr-par"),
443+
DefaultZone: scw.StringPtr("fr-par-1"),
444+
SendTelemetry: scw.BoolPtr(true),
445+
},
446+
Profiles: map[string]*scw.Profile{
447+
"p1": {
448+
AccessKey: scw.StringPtr("SCWP1XXXXXXXXXXXXXXX"),
449+
SecretKey: scw.StringPtr("invalidSecretKey"),
450+
APIURL: scw.StringPtr("https://p1-mock-api-url.com"),
451+
Insecure: scw.BoolPtr(true),
452+
DefaultOrganizationID: scw.StringPtr("11111111-1111-1111-1111-111111111111"),
453+
DefaultRegion: scw.StringPtr("fr-par"),
454+
DefaultZone: scw.StringPtr("fr-par-1"),
455+
SendTelemetry: scw.BoolPtr(true),
456+
},
457+
},
458+
})
459+
}
460+
398461
func createTempConfigFile() (*os.File, error) {
399462
tmpFile, err := os.CreateTemp("", "tmp.yaml")
400463
if err != nil {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
🎲🎲🎲 EXIT CODE: 1 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Invalid access_key 'invalidAccessKey'
4+
5+
Hint:
6+
Access_key should look like: SCWXXXXXXXXXXXXXXXXX.
7+
🟥🟥🟥 JSON STDERR 🟥🟥🟥
8+
{
9+
"message": "invalid access_key 'invalidAccessKey'",
10+
"error": {},
11+
"hint": "access_key should look like: SCWXXXXXXXXXXXXXXXXX."
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
🎲🎲🎲 EXIT CODE: 1 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Invalid secret_key 'invalidSecretKey'
4+
5+
Hint:
6+
Secret_key should be a valid UUID, formatted as: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.
7+
🟥🟥🟥 JSON STDERR 🟥🟥🟥
8+
{
9+
"message": "invalid secret_key 'invalidSecretKey'",
10+
"error": {},
11+
"hint": "secret_key should be a valid UUID, formatted as: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX."
12+
}

0 commit comments

Comments
 (0)