From 2eae01905a91ff6207510f16dc618c6d07b7f7bf Mon Sep 17 00:00:00 2001 From: yuangen Date: Fri, 14 Feb 2025 21:00:04 +0800 Subject: [PATCH] When a reserved variable is set in a parameter, you need to determine whether it is also a query parameter required by the API. --- cli/context.go | 23 ++++-- cli/context_test.go | 12 +++ config/configuration.go | 5 +- config/configuration_test.go | 1 + main/main.go | 1 + meta/repository.go | 36 ++++++++ meta/repository_test.go | 114 ++++++++++++++++++++++++++ openapi/commando.go | 89 ++++++++++++++++++++ openapi/commando_test.go | 155 ++++++++++++++++++++++++++++++++++- openapi/library.go | 5 ++ 10 files changed, 432 insertions(+), 9 deletions(-) diff --git a/cli/context.go b/cli/context.go index 669af2b93..b59e12add 100644 --- a/cli/context.go +++ b/cli/context.go @@ -37,13 +37,18 @@ func NewHelpFlag() *Flag { // CLI Command Context type Context struct { - help bool - flags *FlagSet - unknownFlags *FlagSet - command *Command - completion *Completion - stdout io.Writer - stderr io.Writer + help bool + flags *FlagSet + unknownFlags *FlagSet + command *Command + completion *Completion + stdout io.Writer + stderr io.Writer + inConfigureMode bool +} + +func (ctx *Context) InConfigureMode() bool { + return ctx.inConfigureMode } func NewCommandContext(stdout io.Writer, stderr io.Writer) *Context { @@ -147,3 +152,7 @@ func (ctx *Context) detectFlagByShorthand(ch rune) (*Flag, error) { } return nil, fmt.Errorf("unknown flag -%s", string(ch)) } + +func (ctx *Context) SetInConfigureMode(mode bool) { + ctx.inConfigureMode = mode +} diff --git a/cli/context_test.go b/cli/context_test.go index 0aac38d06..62f7adf09 100644 --- a/cli/context_test.go +++ b/cli/context_test.go @@ -128,3 +128,15 @@ func TestDetectFlagByShorthand(t *testing.T) { assert.Nil(t, f) assert.EqualError(t, err, "unknown flag -c") } + +func TestSetInConfigureMode(t *testing.T) { + w := new(bytes.Buffer) + stderr := new(bytes.Buffer) + ctx := NewCommandContext(w, stderr) + + ctx.SetInConfigureMode(true) + assert.True(t, ctx.InConfigureMode()) + + ctx.SetInConfigureMode(false) + assert.False(t, ctx.InConfigureMode()) +} diff --git a/config/configuration.go b/config/configuration.go index 5ab6071e2..4e0c57d12 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -144,7 +144,10 @@ func LoadProfileWithContext(ctx *cli.Context) (profile Profile, err error) { } // Load from flags - profile.OverwriteWithFlags(ctx) + if ctx.InConfigureMode() { + // If not in configure mode, we will not overwrite the profile with flags + profile.OverwriteWithFlags(ctx) + } err = profile.Validate() return } diff --git a/config/configuration_test.go b/config/configuration_test.go index 97646fab6..584a74e69 100644 --- a/config/configuration_test.go +++ b/config/configuration_test.go @@ -284,6 +284,7 @@ func TestLoadProfileWithContextWhenIGNORE_PROFILE(t *testing.T) { stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) ctx := cli.NewCommandContext(stdout, stderr) + ctx.SetInConfigureMode(true) AddFlags(ctx.Flags()) ctx.Flags().Get("access-key-id").SetAssigned(true) ctx.Flags().Get("access-key-id").SetValue("test-ak-id") diff --git a/main/main.go b/main/main.go index 9c9adbc26..f0f67e8f6 100644 --- a/main/main.go +++ b/main/main.go @@ -63,6 +63,7 @@ func Main(args []string) { ctx := cli.NewCommandContext(stdout, stderr) ctx.EnterCommand(rootCmd) ctx.SetCompletion(cli.ParseCompletionForShell()) + ctx.SetInConfigureMode(openapi.DetectInConfigureMode(ctx.Flags())) rootCmd.AddSubCommand(config.NewConfigureCommand()) rootCmd.AddSubCommand(lib.NewOssCommand()) diff --git a/meta/repository.go b/meta/repository.go index 71ae3d93e..d89371752 100644 --- a/meta/repository.go +++ b/meta/repository.go @@ -15,6 +15,7 @@ package meta import ( "encoding/json" + "regexp" "sort" "strings" @@ -74,6 +75,36 @@ func (a *Repository) GetApi(productCode string, version string, apiName string) return result, true } +func (a *Repository) GetApiByPath(productCode string, version string, method string, path string) (Api, bool) { + var result Api + product, ok := a.GetProduct(productCode) + if !ok { + return result, false + } + // list all apis + for _, apiName := range product.ApiNames { + err := ReadJsonFrom(strings.ToLower(product.Code)+"/"+apiName+".json", &result) + if err != nil { + return result, false + } + // method not equal + if result.Method != method { + continue + } + // replace all [something] to * + // example /permissions/users/[uid]/update to /permissions/users/*/update + var pattern = ReplacePathPattern(result.PathPattern) + // match path + re := regexp.MustCompile("^" + pattern + "$") + if re.MatchString(path) { + result.Product = &product + return result, true + } + } + result = Api{} + return result, false +} + //go:embed versions.json var versions []byte @@ -118,3 +149,8 @@ func (a *Repository) GetStyle(productCode, version string) (string, bool) { return "", false } + +func ReplacePathPattern(pattern string) string { + re := regexp.MustCompile(`\[[^\]]+\]`) + return re.ReplaceAllString(pattern, "[0-9a-zA-Z_\\-\\.{}]+") +} diff --git a/meta/repository_test.go b/meta/repository_test.go index 13212319a..8300de3cc 100644 --- a/meta/repository_test.go +++ b/meta/repository_test.go @@ -50,3 +50,117 @@ func TestGetStyle(t *testing.T) { _, ok = repository.GetStyle("invalid_product", "2016-11-11") assert.False(t, ok) } + +func TestReplacePathPattern(t *testing.T) { + type args struct { + pattern string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test1", + args: args{ + pattern: "/permissions/users/[uid]/update", + }, + want: "/permissions/users/[0-9a-zA-Z_\\-\\.{}]+/update", + }, + { + name: "test2", + args: args{ + pattern: "/permissions/users/[uid]/update/[id]", + }, + want: "/permissions/users/[0-9a-zA-Z_\\-\\.{}]+/update/[0-9a-zA-Z_\\-\\.{}]+", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, ReplacePathPattern(tt.args.pattern), "ReplacePathPattern(%v)", tt.args.pattern) + }) + } +} + +func TestRepository_GetApiByPath(t *testing.T) { + type fields struct { + Products []Product + Names []string + index map[string]Product + } + type args struct { + productCode string + version string + method string + path string + } + tests := []struct { + name string + fields fields + args args + want Api + want1 bool + }{ + { + name: "test1", + fields: fields{ + Products: []Product{ + { + Code: "cs", + ApiNames: []string{"UpdateUserPermissions"}, + }, + }, + Names: []string{"cs"}, + index: map[string]Product{ + "cs": { + Code: "cs", + ApiNames: []string{"UpdateUserPermissions"}, + }, + }, + }, + args: args{ + productCode: "cs", + version: "2015-12-15", + method: "POST", + path: "/permissions/users/xx/update", + }, + want1: true, + }, + { + name: "test2", + fields: fields{ + Products: []Product{ + { + Code: "cs", + ApiNames: []string{"UpdateUserPermissions"}, + }, + }, + Names: []string{"cs"}, + index: map[string]Product{ + "cs": { + Code: "cs", + ApiNames: []string{"UpdateUserPermissions"}, + }, + }, + }, + args: args{ + productCode: "cs", + version: "2015-12-15", + method: "POST", + path: "/permissions/users/xx/update2", + }, + want1: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &Repository{ + Products: tt.fields.Products, + Names: tt.fields.Names, + index: tt.fields.index, + } + _, got1 := a.GetApiByPath(tt.args.productCode, tt.args.version, tt.args.method, tt.args.path) + assert.Equalf(t, tt.want1, got1, "GetApiByPath(%v, %v, %v, %v)", tt.args.productCode, tt.args.version, tt.args.method, tt.args.path) + }) + } +} diff --git a/openapi/commando.go b/openapi/commando.go index 6b74ba102..a8ab6fd01 100644 --- a/openapi/commando.go +++ b/openapi/commando.go @@ -52,6 +52,62 @@ func (c *Commando) InitWithCommand(cmd *cli.Command) { cmd.AutoComplete = c.complete } +func DetectInConfigureMode(flags *cli.FlagSet) bool { + _, modeExist := flags.GetValue(config.ModeFlagName) + if !modeExist { + return true + } + // if mode exist, check if other flags exist + _, akExist := flags.GetValue(config.AccessKeyIdFlagName) + if akExist { + return true + } + _, skExist := flags.GetValue(config.AccessKeySecretFlagName) + if skExist { + return true + } + _, stsExist := flags.GetValue(config.StsTokenFlagName) + if stsExist { + return true + } + // RamRoleNameFlagName + _, ramRoleNameExist := flags.GetValue(config.RamRoleNameFlagName) + if ramRoleNameExist { + return true + } + // RamRoleArnFlagName + _, ramRoleArnExist := flags.GetValue(config.RamRoleArnFlagName) + if ramRoleArnExist { + return true + } + // RoleSessionNameFlagName + _, roleSessionNameExist := flags.GetValue(config.RoleSessionNameFlagName) + if roleSessionNameExist { + return true + } + // PrivateKeyFlagName + _, privateKeyExist := flags.GetValue(config.PrivateKeyFlagName) + if privateKeyExist { + return true + } + // KeyPairNameFlagName + _, keyPairNameExist := flags.GetValue(config.KeyPairNameFlagName) + if keyPairNameExist { + return true + } + // OIDCProviderARNFlagName + _, oidcProviderArnExist := flags.GetValue(config.OIDCProviderARNFlagName) + if oidcProviderArnExist { + return true + } + // OIDCTokenFileFlagName + _, oidcTokenFileExist := flags.GetValue(config.OIDCTokenFileFlagName) + if oidcTokenFileExist { + return true + } + return false +} + func (c *Commando) main(ctx *cli.Context, args []string) error { // aliyun if len(args) == 0 { @@ -59,6 +115,9 @@ func (c *Commando) main(ctx *cli.Context, args []string) error { return nil } + // detect if in configure mode + ctx.SetInConfigureMode(DetectInConfigureMode(ctx.Flags())) + // update current `Profile` with flags var err error c.profile, err = config.LoadProfileWithContext(ctx) @@ -96,14 +155,27 @@ func (c *Commando) main(ctx *cli.Context, args []string) error { } if product.ApiStyle == "restful" { api, _ := c.library.GetApi(product.Code, product.Version, args[1]) + c.CheckApiParamWithBuildInArgs(ctx, api) ctx.Command().Name = args[1] return c.processInvoke(ctx, productName, api.Method, api.PathPattern) + } else { + // RPC need check API parameters too + api, _ := c.library.GetApi(product.Code, product.Version, args[1]) + c.CheckApiParamWithBuildInArgs(ctx, api) } return c.processInvoke(ctx, productName, args[1], "") } else if len(args) == 3 { // restful call // aliyun {GET|PUT|POST|DELETE} -- + product, _ := c.library.GetProduct(productName) + api, find := c.library.GetApiByPath(product.Code, product.Version, args[1], args[2]) + if !find { + // throw error, can not find api by path + return cli.NewErrorWithTip(fmt.Errorf("can not find api by path %s", args[2]), + "Please confirm if the API path exists") + } + c.CheckApiParamWithBuildInArgs(ctx, api) return c.processInvoke(ctx, productName, args[1], args[2]) } else { return cli.NewErrorWithTip(fmt.Errorf("too many arguments"), @@ -418,3 +490,20 @@ func (c *Commando) printUsage(ctx *cli.Context) { cmd.PrintSample(ctx) cmd.PrintTail(ctx) } + +func (c *Commando) CheckApiParamWithBuildInArgs(ctx *cli.Context, api meta.Api) { + for _, p := range api.Parameters { + // 如果参数中包含了known参数,且 known参数已经被赋值,则将 known 参数拷贝给 unknown 参数 + if ep, ok := ctx.Flags().GetValue(p.Name); ok { + if p.Position != "Query" { + continue + } + var flagNew = &cli.Flag{ + Name: p.Name, + } + flagNew.SetValue(ep) + flagNew.SetAssigned(true) + ctx.UnknownFlags().Add(flagNew) + } + } +} diff --git a/openapi/commando_test.go b/openapi/commando_test.go index 0ef3fafe9..f9d913944 100644 --- a/openapi/commando_test.go +++ b/openapi/commando_test.go @@ -105,7 +105,7 @@ func Test_main(t *testing.T) { args = []string{"test", "test2", "test1"} err = command.main(ctx, args) assert.NotNil(t, err) - assert.Equal(t, "'test' is not a valid command or product. See `aliyun help`.", err.Error()) + assert.Equal(t, "can not find api by path test1", err.Error()) args = []string{"test", "test2", "test1", "test3"} err = command.main(ctx, args) @@ -344,3 +344,156 @@ func TestCreateInvoker(t *testing.T) { assert.Nil(t, err) } + +func TestCheckApiParamWithBuildInArgs(t *testing.T) { + // Initialize cli.Context + w := new(bytes.Buffer) + stderr := new(bytes.Buffer) + ctx := cli.NewCommandContext(w, stderr) + cmd := &cli.Command{} + cmd.EnableUnknownFlag = true + ctx.EnterCommand(cmd) + + // Add known flags to context + knownFlag := &cli.Flag{ + Name: "KnownParam", + } + knownFlag.SetValue("KnownValue") + knownFlag.SetAssigned(true) + ctx.Flags().Add(knownFlag) + + // Initialize meta.Api with parameters + api := meta.Api{ + Parameters: []meta.Parameter{ + { + Name: "KnownParam", + Position: "Query", + }, + { + Name: "UnknownParam", + Position: "Query", + }, + }, + } + + // Create Commando instance + profile := config.Profile{ + Language: "en", + RegionId: "cn-hangzhou", + } + commando := NewCommando(w, profile) + + // Call CheckApiParamWithBuildInArgs + commando.CheckApiParamWithBuildInArgs(ctx, api) + + // Verify unknown flags + unknownFlag, ok := ctx.UnknownFlags().GetValue("KnownParam") + assert.True(t, ok) + assert.Equal(t, "KnownValue", unknownFlag) +} + +func TestDetectInConfigureMode(t *testing.T) { + // Test case 1: No flags set + flags := cli.NewFlagSet() + result := DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when no flags are set") + + // Test case 2: Mode flag set + flags = cli.NewFlagSet() + modeFlag := &cli.Flag{Name: config.ModeFlagName} + modeFlag.SetAssigned(true) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.False(t, result, "Expected false when mode flag is set") + + // Test case 3: AccessKeyId flag set + flags = cli.NewFlagSet() + modeFlag.SetAssigned(true) + flags.Add(modeFlag) + akFlag := &cli.Flag{Name: config.AccessKeyIdFlagName} + akFlag.SetAssigned(true) + flags.Add(akFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when AccessKeyId flag is set") + + // Test case 4: AccessKeySecret flag set + flags = cli.NewFlagSet() + skFlag := &cli.Flag{Name: config.AccessKeySecretFlagName} + skFlag.SetAssigned(true) + flags.Add(skFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when AccessKeySecret flag is set") + + // Test case 5: StsToken flag set + flags = cli.NewFlagSet() + stsFlag := &cli.Flag{Name: config.StsTokenFlagName} + stsFlag.SetAssigned(true) + flags.Add(stsFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when StsToken flag is set") + + // Test case 6: RamRoleName flag set + flags = cli.NewFlagSet() + ramRoleNameFlag := &cli.Flag{Name: config.RamRoleNameFlagName} + ramRoleNameFlag.SetAssigned(true) + flags.Add(ramRoleNameFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when RamRoleName flag is set") + + // Test case 7: RamRoleArn flag set + flags = cli.NewFlagSet() + ramRoleArnFlag := &cli.Flag{Name: config.RamRoleArnFlagName} + ramRoleArnFlag.SetAssigned(true) + flags.Add(ramRoleArnFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when RamRoleArn flag is set") + + // Test case 8: RoleSessionName flag set + flags = cli.NewFlagSet() + roleSessionNameFlag := &cli.Flag{Name: config.RoleSessionNameFlagName} + roleSessionNameFlag.SetAssigned(true) + flags.Add(roleSessionNameFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when RoleSessionName flag is set") + + // Test case 9: PrivateKey flag set + flags = cli.NewFlagSet() + privateKeyFlag := &cli.Flag{Name: config.PrivateKeyFlagName} + privateKeyFlag.SetAssigned(true) + flags.Add(privateKeyFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when PrivateKey flag is set") + + // Test case 10: KeyPairName flag set + flags = cli.NewFlagSet() + keyPairNameFlag := &cli.Flag{Name: config.KeyPairNameFlagName} + keyPairNameFlag.SetAssigned(true) + flags.Add(keyPairNameFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when KeyPairName flag is set") + + // Test case 11: OIDCProviderARN flag set + flags = cli.NewFlagSet() + oidcProviderArnFlag := &cli.Flag{Name: config.OIDCProviderARNFlagName} + oidcProviderArnFlag.SetAssigned(true) + flags.Add(oidcProviderArnFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when OIDCProviderARN flag is set") + + // Test case 12: OIDCTokenFile flag set + flags = cli.NewFlagSet() + oidcTokenFileFlag := &cli.Flag{Name: config.OIDCTokenFileFlagName} + oidcTokenFileFlag.SetAssigned(true) + flags.Add(oidcTokenFileFlag) + flags.Add(modeFlag) + result = DetectInConfigureMode(flags) + assert.True(t, result, "Expected true when OIDCTokenFile flag is set") +} diff --git a/openapi/library.go b/openapi/library.go index 66375ddb1..b3607bcd1 100644 --- a/openapi/library.go +++ b/openapi/library.go @@ -46,6 +46,11 @@ func (a *Library) GetApi(productCode string, version string, apiName string) (me return a.builtinRepo.GetApi(productCode, version, apiName) } +func (a *Library) GetApiByPath(productCode string, version string, method string, path string) (meta.Api, bool) { + return a.builtinRepo.GetApiByPath(productCode, version, method, path) + +} + func (a *Library) GetStyle(productCode string, version string) (string, bool) { return a.builtinRepo.GetStyle(productCode, version) }