diff --git a/core/bootstrap.go b/core/bootstrap.go index f8dcf9ff2e..df4933e771 100644 --- a/core/bootstrap.go +++ b/core/bootstrap.go @@ -236,7 +236,12 @@ func Bootstrap(config *BootstrapConfig) (exitCode int, result any, err error) { // Run checks after command has been executed defer func() { // if we plan to remove defer, do not forget logger is not set until cobra pre init func // Check CLI new version and api key expiration date - runAfterCommandChecks(ctx, config.BuildInfo.checkVersion, checkAPIKey) + runAfterCommandChecks( + ctx, + config.BuildInfo.checkVersion, + checkAPIKey, + checkIfMultipleVariableSources, + ) }() if !config.DisableAliases { diff --git a/core/checks.go b/core/checks.go index 6f257c6f0b..e17f6d4f15 100644 --- a/core/checks.go +++ b/core/checks.go @@ -1,13 +1,16 @@ package core import ( + "bytes" "context" "fmt" "os" "path/filepath" + "reflect" "time" iam "github.com/scaleway/scaleway-sdk-go/api/iam/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" ) var ( @@ -15,6 +18,10 @@ var ( lastChecksFileLocalName = "last-cli-checks" ) +const ( + defaultCredentialSource = "environment variable" +) + type AfterCommandCheckFunc func(ctx context.Context) // wasFileModifiedLast24h checks whether the file has been updated during last 24 hours. @@ -105,3 +112,43 @@ func checkAPIKey(ctx context.Context) { ExtractLogger(ctx).Warningf("Current api key expires in %s\n", expiresIn) } } + +// checkIfMultipleVariableSources return an informative message during the CLI initialization +// if there are multiple sources of configuration that could confuse the user +func checkIfMultipleVariableSources(ctx context.Context) { + config, err := scw.LoadConfigFromPath(ExtractConfigPath(ctx)) + if err != nil { + return + } + + activeProfile, err := config.GetActiveProfile() + if err != nil { + return + } + + profileEnv := scw.LoadEnvProfile() + + vFile := reflect.ValueOf(activeProfile).Elem() + vEnv := reflect.ValueOf(profileEnv).Elem() + t := vFile.Type() + + var buffer bytes.Buffer + buffer.WriteString("Checking multiple variable sources: \n") + + for i := range t.NumField() { + valFile := vFile.Field(i) + valEnv := vEnv.Field(i) + + if !valFile.IsNil() && !valEnv.IsNil() { + if valFile.Elem().String() != valEnv.Elem().String() { + buffer.WriteString(fmt.Sprintf( + "- Variable '%s' is defined in both config.yaml and environment with different values. Using: %s.\n", + t.Field(i).Name, + defaultCredentialSource, + )) + } + } + } + + ExtractLogger(ctx).Warning(buffer.String()) +} diff --git a/core/checks_test.go b/core/checks_test.go index 58a2681d6b..6470e8c76f 100644 --- a/core/checks_test.go +++ b/core/checks_test.go @@ -86,3 +86,54 @@ func TestCheckAPIKey(t *testing.T) { }, })) } + +func TestCheckIfMultipleVariableSources(t *testing.T) { + testCommands := core.NewCommands( + &core.Command{ + Namespace: "test", + ArgSpecs: core.ArgSpecs{}, + ArgsType: reflect.TypeOf(testType{}), + Run: func(ctx context.Context, _ any) (any, error) { return "", nil }, + }, + ) + + t.Run("conflicting sources should trigger warning", core.Test(&core.TestConfig{ + Commands: testCommands, + TmpHomeDir: true, + BeforeFunc: func(ctx *core.BeforeFuncCtx) error { + cfg := &scw.Config{ + Profile: scw.Profile{ + AccessKey: scw.StringPtr("SCW11111111111111111"), + SecretKey: scw.StringPtr("config-secret"), + DefaultProjectID: scw.StringPtr("config-project-id"), + }, + } + + configPath := filepath.Join(ctx.OverrideEnv["HOME"], ".config", "scw", "config.yaml") + if err := cfg.SaveTo(configPath); err != nil { + return err + } + + t.Setenv("SCW_ACCESS_KEY", "SCW99999999999999999") + t.Setenv("SCW_SECRET_KEY", "env-secret") + t.Setenv("SCW_DEFAULT_PROJECT_ID", "config-project-id") + + return nil + }, + Cmd: "scw test", + Check: core.TestCheckCombine( + core.TestCheckExitCode(0), + func(t *testing.T, ctx *core.CheckFuncCtx) { + t.Helper() + expected := "" + + "Checking multiple variable sources: \n" + + "- Variable 'AccessKey' is defined in both config.yaml and environment with different values. " + + "Using: environment variable.\n" + + "- Variable 'SecretKey' is defined in both config.yaml and environment with different values. " + + "Using: environment variable.\n\n" + + assert.Equal(t, expected, ctx.LogBuffer) + }, + ), + })) +} diff --git a/core/testdata/test-check-if-multiple-variable-sources-conflicting-sources-should-trigger-warning.cassette.yaml b/core/testdata/test-check-if-multiple-variable-sources-conflicting-sources-should-trigger-warning.cassette.yaml new file mode 100644 index 0000000000..4a710de37c --- /dev/null +++ b/core/testdata/test-check-if-multiple-variable-sources-conflicting-sources-should-trigger-warning.cassette.yaml @@ -0,0 +1,35 @@ +--- +version: 1 +interactions: +- request: + body: '{"message":"authentication is denied","method":"api_key","reason":"not_found","type":"denied_authentication"}' + form: {} + headers: + User-Agent: + - scaleway-sdk-go/v1.0.0-beta.7+dev (go1.24.4; darwin; arm64) cli-e2e-test + url: https://api.scaleway.com/iam/v1alpha1/api-keys/SCWXXXXXXXXXXXXXXXXX + method: GET + response: + body: '{"message":"authentication is denied","method":"api_key","reason":"not_found","type":"denied_authentication"}' + headers: + Content-Length: + - "109" + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Tue, 22 Jul 2025 12:16:22 GMT + Server: + - Scaleway API Gateway (fr-par-1;edge01) + Strict-Transport-Security: + - max-age=63072000 + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - DENY + X-Request-Id: + - d55dda1c-064c-4670-95ff-50d7c32fe521 + status: 401 Unauthorized + code: 401 + duration: ""