diff --git a/.gitignore b/.gitignore index f159510d..5f850d1d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea .vscode ./gcoreclient +.go/ diff --git a/client/faas/v1/client/client.go b/client/faas/v1/client/client.go new file mode 100644 index 00000000..9d87cb5b --- /dev/null +++ b/client/faas/v1/client/client.go @@ -0,0 +1,16 @@ +package client + +import ( + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/common" + + "github.com/urfave/cli/v2" +) + +func NewFaaSClientV1(c *cli.Context) (*gcorecloud.ServiceClient, error) { + return common.BuildClient(c, "faas/namespaces", "v1") +} + +func NewFaaSKeysClientV1(c *cli.Context) (*gcorecloud.ServiceClient, error) { + return common.BuildClient(c, "faas/keys", "v1") +} diff --git a/client/faas/v1/functions/functions.go b/client/faas/v1/functions/functions.go new file mode 100644 index 00000000..f5fc05d6 --- /dev/null +++ b/client/faas/v1/functions/functions.go @@ -0,0 +1,589 @@ +package functions + +import ( + "fmt" + "strings" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/faas/v1/client" + "github.com/G-Core/gcorelabscloud-go/client/faas/v1/keys" + "github.com/G-Core/gcorelabscloud-go/client/faas/v1/namespaces" + "github.com/G-Core/gcorelabscloud-go/client/flags" + "github.com/G-Core/gcorelabscloud-go/client/utils" + "github.com/G-Core/gcorelabscloud-go/gcore/faas/v1/faas" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + + "github.com/urfave/cli/v2" +) + +const functionNameText = "function_name is mandatory argument" + +var Commands = cli.Command{ + Name: "functions", + Usage: "GCloud FaaS functions API", + Subcommands: []*cli.Command{ + &namespaces.Commands, + &keys.Commands, + &functionListCommand, + &functionShowCommand, + &functionDeleteCommand, + &functionCreateCommand, + &functionUpdateCommand, + &functionSaveCommand, + }, +} + +var functionListCommand = cli.Command{ + Name: "list", + Usage: "List functions", + Category: "functions", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Aliases: []string{"ns"}, + Usage: "show functions in namespace", + Required: true, + }, + &cli.StringFlag{ + Name: "search", + Aliases: []string{"s"}, + Usage: "show functions whose names contain provided value", + Required: false, + }, + &cli.IntFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "limit the number of returned functions. Limited by max limit value of 1000", + Required: false, + }, + &cli.IntFlag{ + Name: "offset", + Aliases: []string{"o"}, + Usage: "offset value is used to exclude the first set of records from the result", + Required: false, + }, + &cli.StringFlag{ + Name: "order", + Usage: "order functions by transmitted fields and directions (name.asc).", + Required: false, + }, + }, + Action: func(c *cli.Context) error { + return listFunctions(c) + }, +} + +func listFunctions(c *cli.Context) error { + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + opts := faas.ListOpts{ + Limit: c.Int("limit"), + Offset: c.Int("offset"), + Search: c.String("search"), + OrderBy: c.String("order"), + } + + results, err := faas.ListFunctionsALL(cl, c.String("namespace"), opts) + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(results, c.String("format")) + return nil +} + +var functionShowCommand = cli.Command{ + Name: "show", + Usage: "Show function", + Category: "functions", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Aliases: []string{"ns"}, + Usage: "function namespace", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + return showFunction(c) + }, +} + +func showFunction(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, functionNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + function, err := faas.GetFunction(cl, c.String("namespace"), name).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(function, c.String("format")) + return nil +} + +var functionDeleteCommand = cli.Command{ + Name: "delete", + Usage: "Delete function.", + Category: "functions", + ArgsUsage: "", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Aliases: []string{"ns"}, + Usage: "function namespace", + Required: true, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + return deleteFunction(c) + }, +} + +func deleteFunction(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, functionNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "delete") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + results, err := faas.DeleteFunction(cl, c.String("namespace"), name).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + return utils.WaitTaskAndShowResult(c, cl, results, false, func(task tasks.TaskID) (interface{}, error) { + _, err := faas.GetFunction(cl, c.String("namespace"), name).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete function with Name: %s", name) + } + + switch err.(type) { + case gcorecloud.ErrDefault404: + return nil, nil + default: + return nil, err + } + }) +} + +var functionCreateCommand = cli.Command{ + Name: "create", + Usage: "create function.", + Category: "functions", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Aliases: []string{"ns"}, + Usage: "function namespace", + Required: true, + }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "function name", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "envs", + Aliases: []string{"e"}, + Usage: "environment variables. 'env_name'='value'", + Required: false, + }, + &cli.PathFlag{ + Name: "file", + Usage: "file where function declared", + Required: true, + }, + &cli.BoolFlag{ + Name: "disabled", + Usage: "if true function will not be active from start.", + Required: false, + }, + &cli.StringSliceFlag{ + Name: "keys", + Usage: "list of used List of used authentication api keys.", + Required: false, + }, + &cli.BoolFlag{ + Name: "enable_api_key", + Usage: "if true function will require API keys from 'keys' list for authorization.", + Required: false, + }, + &cli.StringFlag{ + Name: "dependencies", + Usage: "function dependencies to install", + Required: false, + }, + &cli.StringFlag{ + Name: "flavor", + Aliases: []string{"fl"}, + Usage: "flavor name", + Required: true, + }, + &cli.StringFlag{ + Name: "method", + Usage: "main startup method name", + Required: true, + }, + &cli.StringFlag{ + Name: "runtime", + Usage: "function runtime", + Required: true, + }, + &cli.StringFlag{ + Name: "description", + Usage: "function description", + Required: false, + }, + &cli.IntFlag{ + Name: "timeout", + Usage: "function timeout in seconds", + Required: false, + }, + &cli.IntFlag{ + Name: "scale_min", + Usage: "autoscale from", + Aliases: []string{"min"}, + Value: 0, + Required: false, + }, + &cli.IntFlag{ + Name: "scale_max", + Usage: "autoscale to", + Aliases: []string{"max"}, + Value: 1, + Required: false, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + return createFunction(c) + }, +} + +func createFunction(c *cli.Context) error { + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + code, err := extractCodeFromFile(c.Path("file")) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + disabled := c.Bool("disabled") + enableApiKey := c.Bool("enable_api_key") + opts := faas.CreateFunctionOpts{ + Name: c.String("name"), + Description: c.String("description"), + Envs: extractEnvs(c.StringSlice("envs")), + Runtime: c.String("runtime"), + Timeout: c.Int("timeout"), + Flavor: c.String("flavor"), + CodeText: code, + Disabled: &disabled, + Keys: c.StringSlice("keys"), + Dependencies: c.String("dependencies"), + EnableApiKey: &enableApiKey, + MainMethod: c.String("method"), + } + + if c.IsSet("scale_min") || c.IsSet("scale_max") { + min := c.Int("scale_min") + max := c.Int("scale_max") + opts.Autoscaling = faas.FunctionAutoscaling{ + MinInstances: &min, + MaxInstances: &max, + } + } + + results, err := faas.CreateFunction(cl, c.String("namespace"), opts).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + return utils.WaitTaskAndShowResult(c, cl, results, true, func(task tasks.TaskID) (interface{}, error) { + _, err := tasks.Get(cl, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + + fn, err := faas.GetFunction(cl, c.String("namespace"), c.String("name")).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get function with Name: %s. Error: %w", c.String("name"), err) + } + + return fn, nil + }) +} + +func extractEnvs(list []string) map[string]string { + envs := make(map[string]string) + for _, item := range list { + ss := strings.Split(item, "=") + envs[ss[0]] = ss[1] + } + + return envs +} + +func extractCodeFromFile(filePath string) (string, error) { + ok, err := utils.FileExists(filePath) + if err != nil { + return "", err + } + + if !ok { + return "", fmt.Errorf("file '%s' isn't exist", filePath) + } + + bytes, err := utils.ReadFile(filePath) + if err != nil { + return "", err + } + + return string(bytes), nil +} + +var functionUpdateCommand = cli.Command{ + Name: "update", + Usage: "update function.", + ArgsUsage: "", + Category: "functions", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Aliases: []string{"ns"}, + Usage: "function namespace", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "envs", + Aliases: []string{"e"}, + Usage: "environment variables. 'env_name'='value'", + Required: false, + }, + &cli.PathFlag{ + Name: "file", + Usage: "file where function declared", + Required: false, + }, + &cli.BoolFlag{ + Name: "disabled", + Usage: "if true function will not be active from start.", + Required: false, + }, + &cli.StringFlag{ + Name: "dependencies", + Usage: "function dependencies to install", + }, + &cli.StringSliceFlag{ + Name: "keys", + Usage: "list of used List of used authentication api keys.", + Required: false, + }, + &cli.BoolFlag{ + Name: "enable_api_key", + Usage: "if true function will require API keys from 'keys' list for authorization.", + Required: false, + }, + &cli.StringFlag{ + Name: "flavor", + Aliases: []string{"fl"}, + Usage: "flavor name", + Required: false, + }, + &cli.StringFlag{ + Name: "method", + Usage: "main startup method name", + Required: false, + }, + &cli.IntFlag{ + Name: "timeout", + Usage: "function timeout in seconds", + Required: false, + }, + &cli.IntFlag{ + Name: "scale_min", + Usage: "autoscale from", + Aliases: []string{"min"}, + Required: false, + }, + &cli.IntFlag{ + Name: "scale_max", + Usage: "autoscale to", + Aliases: []string{"max"}, + Required: false, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + return updateFunction(c) + }, +} + +func updateFunction(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, functionNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "update") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + opts := faas.UpdateFunctionOpts{} + + if c.IsSet("dependencies") { + opts.Dependencies = c.String("dependencies") + } + + if c.IsSet("envs") { + opts.Envs = extractEnvs(c.StringSlice("envs")) + } + + if c.IsSet("file") { + opts.CodeText, err = extractCodeFromFile(c.Path("file")) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + } + + if c.IsSet("disabled") { + disabled := c.Bool("disabled") + opts.Disabled = &disabled + } + + if c.IsSet("keys") { + ss := c.StringSlice("keys") + opts.Keys = &ss + } + + if c.IsSet("enable_api_key") { + apiKey := c.Bool("enable_api_key") + opts.EnableApiKey = &apiKey + } + + if c.IsSet("flavor") { + opts.Flavor = c.String("flavor") + } + + if c.IsSet("method") { + opts.MainMethod = c.String("method") + } + + if c.IsSet("timeout") { + opts.Timeout = c.Int("timeout") + } + + opts.Autoscaling = &faas.FunctionAutoscaling{} + if c.IsSet("scale_min") { + min := c.Int("scale_min") + opts.Autoscaling.MinInstances = &min + } + + if c.IsSet("scale_max") { + max := c.Int("scale_max") + opts.Autoscaling.MaxInstances = &max + } + + results, err := faas.UpdateFunction(cl, c.String("namespace"), name, opts).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + return utils.WaitTaskAndShowResult(c, cl, results, true, func(task tasks.TaskID) (interface{}, error) { + _, err := tasks.Get(cl, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + + fn, err := faas.GetFunction(cl, c.String("namespace"), name).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get function with Name: %s. Error: %w", name, err) + } + + return fn, nil + }) +} + +var functionSaveCommand = cli.Command{ + Name: "save", + Usage: "saves function to file.", + ArgsUsage: "", + Category: "functions", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "namespace", + Aliases: []string{"ns"}, + Usage: "function namespace", + Required: true, + }, + &cli.PathFlag{ + Name: "file", + Usage: "file path", + Required: false, + }, + }, + Action: func(c *cli.Context) error { + return saveFunction(c) + }, +} + +func saveFunction(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, functionNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "save") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + fn, err := faas.GetFunction(cl, c.String("namespace"), name).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + var file string + if c.IsSet("file") { + file = c.Path("file") + } else { + file = fmt.Sprintf("%s-%s.%s", fn.Name, c.String("namespace"), fn.Runtime[0:2]) + } + + if err := utils.WriteToFile(file, []byte(fn.CodeText)); err != nil { + return cli.Exit(err, 1) + } + + return nil +} diff --git a/client/faas/v1/keys/keys.go b/client/faas/v1/keys/keys.go new file mode 100644 index 00000000..821e212e --- /dev/null +++ b/client/faas/v1/keys/keys.go @@ -0,0 +1,330 @@ +package keys + +import ( + "strings" + "time" + + "github.com/urfave/cli/v2" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/faas/v1/client" + "github.com/G-Core/gcorelabscloud-go/client/flags" + "github.com/G-Core/gcorelabscloud-go/client/utils" + "github.com/G-Core/gcorelabscloud-go/gcore/faas/v1/faas" +) + +const keyNameText = "key_name is mandatory argument" + +var Commands = cli.Command{ + Name: "keys", + Usage: "GCloud FaaS keys API", + Subcommands: []*cli.Command{ + &keyListCommand, + &keyShowCommand, + &keyCreateCommand, + &keyUpdateCommand, + &keyDeleteCommand, + }, +} + +var keyListCommand = cli.Command{ + Name: "list", + Usage: "List API keys.", + Category: "api keys", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "search", + Aliases: []string{"s"}, + Usage: "show keys whose names contain provided value", + Required: false, + }, + &cli.IntFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "limit the number of returned keys. Limited by max limit value of 1000", + Required: false, + }, + &cli.IntFlag{ + Name: "offset", + Aliases: []string{"o"}, + Usage: "offset value is used to exclude the first set of records from the result", + Required: false, + }, + &cli.StringFlag{ + Name: "order", + Usage: "order keys by transmitted fields and directions (name.asc).", + Required: false, + }, + }, + Action: func(c *cli.Context) error { + return listKeys(c) + }, +} + +func listKeys(c *cli.Context) error { + cl, err := client.NewFaaSKeysClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + } + + opts := faas.ListOpts{ + Limit: c.Int("limit"), + Offset: c.Int("offset"), + Search: c.String("search"), + OrderBy: c.String("order"), + } + + results, err := faas.ListKeysAll(cl, opts) + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(results, c.String("format")) + + return nil +} + +var keyShowCommand = cli.Command{ + Name: "show", + Usage: "Show API keys.", + Category: "api keys", + ArgsUsage: "", + Action: func(c *cli.Context) error { + return showKey(c) + }, +} + +func showKey(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, keyNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + + cl, err := client.NewFaaSKeysClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + key, err := faas.GetKey(cl, name).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(key, c.String("format")) + return nil +} + +var keyCreateCommand = cli.Command{ + Name: "create", + Usage: "Create API key.", + Category: "api keys", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "key name", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "functions", + Aliases: []string{"fs"}, + Usage: "", + Required: false, + }, + &cli.StringFlag{ + Name: "description", + Usage: "key description", + Required: false, + }, + &cli.StringFlag{ + Name: "expire", + Usage: "when key will expire. Format 2023-07-31T00:00:00Z", + Required: false, + }, + &cli.PathFlag{ + Name: "file", + Usage: "where to put API key secret", + Required: false, + }, + }, + Action: func(c *cli.Context) error { + return createKey(c) + }, +} + +func createKey(c *cli.Context) error { + cl, err := client.NewFaaSKeysClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + opts := faas.CreateKeyOpts{ + Name: c.String("name"), + Description: c.String("description"), + } + + if c.IsSet("expire") { + expireRaw := c.String("expire") + t, err := time.Parse(gcorecloud.RFC3339ZZ, expireRaw) + if err != nil { + _ = cli.ShowCommandHelp(c, "update") + return cli.Exit("Invalid format for functions", 1) + } + expire := gcorecloud.JSONRFC3339ZZ{Time: t} + opts.Expire = &expire + } + + if c.IsSet("functions") { + var functions []faas.KeysFunction + items := c.StringSlice("functions") + for _, item := range items { + ss := strings.Split(item, "/") + if len(ss) != 2 { + _ = cli.ShowCommandHelp(c, "create") + return cli.Exit("Invalid format for functions", 1) + } + functions = append(functions, faas.KeysFunction{ + Name: ss[1], + Namespace: ss[0], + }) + } + opts.Functions = functions + } + + key, err := faas.CreateKey(cl, opts) + if err != nil { + return cli.Exit(err, 1) + } + + if c.IsSet("file") { + secret := key.Secret + if err := utils.WriteToFile(c.Path("file"), []byte(secret)); err != nil { + return cli.Exit(err, 1) + } + } + + utils.ShowResults(key, c.String("format")) + + return nil +} + +var keyUpdateCommand = cli.Command{ + Name: "update", + Usage: "Update API key.", + ArgsUsage: "", + Category: "api keys", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "functions", + Aliases: []string{"fs"}, + Usage: "", + Required: false, + }, + &cli.StringFlag{ + Name: "description", + Usage: "key description", + Required: false, + }, + &cli.StringFlag{ + Name: "expire", + Usage: "when key will expire. Format 2023-07-31T00:00:00Z", + Required: false, + }, + }, + Action: func(c *cli.Context) error { + return updateKey(c) + }, +} + +func updateKey(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, keyNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "update") + return err + } + + cl, err := client.NewFaaSKeysClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + opts := faas.UpdateKeyOpts{} + + if c.IsSet("description") { + opts.Description = c.String("description") + } + + if c.IsSet("expire") { + expireRaw := c.String("expire") + t, err := time.Parse(gcorecloud.RFC3339ZZ, expireRaw) + if err != nil { + _ = cli.ShowCommandHelp(c, "update") + return cli.Exit("Invalid format for functions", 1) + } + expire := gcorecloud.JSONRFC3339ZZ{Time: t} + opts.Expire = &expire + } + + if c.IsSet("functions") { + var functions []faas.KeysFunction + items := c.StringSlice("functions") + for _, item := range items { + ss := strings.Split(item, "/") + if len(ss) != 2 { + _ = cli.ShowCommandHelp(c, "update") + return cli.Exit("Invalid format for functions", 1) + } + functions = append(functions, faas.KeysFunction{ + Name: ss[1], + Namespace: ss[0], + }) + } + + opts.Functions = functions + } + + key, err := faas.UpdateKey(cl, name, opts) + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(key, c.String("format")) + + return nil +} + +var keyDeleteCommand = cli.Command{ + Name: "delete", + Usage: "Delete API key.", + Category: "api keys", + ArgsUsage: "", + Action: func(c *cli.Context) error { + return deleteKey(c) + }, +} + +func deleteKey(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, keyNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "update") + return err + } + + cl, err := client.NewFaaSKeysClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + err = faas.DeleteKey(cl, name) + switch err.(type) { + case gcorecloud.ErrDefault404: + return nil + default: + return cli.Exit(err, 1) + } +} diff --git a/client/faas/v1/namespaces/namespaces.go b/client/faas/v1/namespaces/namespaces.go new file mode 100644 index 00000000..f42316d1 --- /dev/null +++ b/client/faas/v1/namespaces/namespaces.go @@ -0,0 +1,302 @@ +package namespaces + +import ( + "fmt" + "strings" + + gcorecloud "github.com/G-Core/gcorelabscloud-go" + "github.com/G-Core/gcorelabscloud-go/client/faas/v1/client" + "github.com/G-Core/gcorelabscloud-go/client/flags" + "github.com/G-Core/gcorelabscloud-go/client/utils" + "github.com/G-Core/gcorelabscloud-go/gcore/faas/v1/faas" + "github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks" + + "github.com/urfave/cli/v2" +) + +const ( + namespaceNameText = "namespace_name is mandatory argument" +) + +var Commands = cli.Command{ + Name: "namespaces", + Usage: "GCloud FaaS namespaces API", + Subcommands: []*cli.Command{ + &namespaceListCommand, + &namespaceShowCommand, + &namespaceCreateCommand, + &namespaceUpdateCommand, + &namespaceDeleteCommand, + }, +} + +var namespaceListCommand = cli.Command{ + Name: "list", + Usage: "Display list of namespaces", + Category: "namespaces", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "search", + Aliases: []string{"s"}, + Usage: "show namespaces whose names contain provided value", + Required: false, + }, + &cli.IntFlag{ + Name: "limit", + Aliases: []string{"l"}, + Usage: "limit the number of returned namespaces. Limited by max limit value of 1000", + Required: false, + }, + &cli.IntFlag{ + Name: "offset", + Aliases: []string{"o"}, + Usage: "offset value is used to exclude the first set of records from the result", + Required: false, + }, + &cli.StringFlag{ + Name: "order", + Usage: "order namespaces by transmitted fields and directions (name.asc).", + Required: false, + }, + }, + Action: func(c *cli.Context) error { + return listNamespaces(c) + }, +} + +func listNamespaces(c *cli.Context) error { + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + opts := faas.ListOpts{ + Limit: c.Int("limit"), + Offset: c.Int("offset"), + Search: c.String("search"), + OrderBy: c.String("order"), + } + + results, err := faas.ListNamespaceALL(cl, opts) + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(results, c.String("format")) + + return nil +} + +var namespaceShowCommand = cli.Command{ + Name: "show", + Usage: "Show namespace", + Category: "namespaces", + ArgsUsage: "", + Action: func(c *cli.Context) error { + return showNamespace(c) + }, +} + +func showNamespace(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, namespaceNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + namespace, err := faas.GetNamespace(cl, name).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + utils.ShowResults(namespace, c.String("format")) + + return nil +} + +var namespaceCreateCommand = cli.Command{ + Name: "create", + Usage: "create namespace.", + Category: "namespaces", + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "function name", + Required: true, + }, + &cli.StringSliceFlag{ + Name: "envs", + Aliases: []string{"e"}, + Usage: "environment variables. 'env_name'='value'", + Required: false, + }, + &cli.StringFlag{ + Name: "description", + Usage: "function description", + Required: false, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + return createNamespace(c) + }, +} + +func extractEnvs(list []string) map[string]string { + envs := make(map[string]string) + for _, item := range list { + ss := strings.Split(item, "=") + envs[ss[0]] = ss[1] + } + + return envs +} + +func createNamespace(c *cli.Context) error { + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + opts := faas.CreateNamespaceOpts{ + Name: c.String("name"), + Description: c.String("description"), + Envs: extractEnvs(c.StringSlice("envs")), + } + + results, err := faas.CreateNamespace(cl, opts).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + return utils.WaitTaskAndShowResult(c, cl, results, true, func(task tasks.TaskID) (interface{}, error) { + _, err := tasks.Get(cl, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + + ns, err := faas.GetNamespace(cl, c.String("name")).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get namespace with Name: %s. Error: %w", c.String("name"), err) + } + + return ns, nil + }) +} + +var namespaceUpdateCommand = cli.Command{ + Name: "update", + Usage: "update namespace.", + Category: "namespaces", + ArgsUsage: "", + Flags: append([]cli.Flag{ + &cli.StringSliceFlag{ + Name: "envs", + Aliases: []string{"e"}, + Usage: "environment variables. 'env_name'='value'", + Required: false, + }, + &cli.StringFlag{ + Name: "description", + Usage: "namespace description", + Required: false, + }, + }, flags.WaitCommandFlags...), + Action: func(c *cli.Context) error { + return updateNamespace(c) + }, +} + +func updateNamespace(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, namespaceNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "update") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + var opts faas.UpdateNamespaceOpts + if c.IsSet("description") { + opts.Description = c.String("description") + } + + if c.IsSet("envs") { + opts.Envs = extractEnvs(c.StringSlice("envs")) + } + + results, err := faas.UpdateNamespace(cl, name, opts).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + return utils.WaitTaskAndShowResult(c, cl, results, true, func(task tasks.TaskID) (interface{}, error) { + _, err := tasks.Get(cl, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + + ns, err := faas.GetNamespace(cl, name).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get namespace with Name: %s. Error: %w", name, err) + } + + return ns, nil + }) +} + +var namespaceDeleteCommand = cli.Command{ + Name: "delete", + Usage: "Delete namespace", + Category: "namespaces", + ArgsUsage: "", + Flags: flags.WaitCommandFlags, + Action: func(c *cli.Context) error { + return deleteNamespace(c) + }, +} + +func deleteNamespace(c *cli.Context) error { + name, err := flags.GetFirstStringArg(c, namespaceNameText) + if err != nil { + _ = cli.ShowCommandHelp(c, "show") + return err + } + + cl, err := client.NewFaaSClientV1(c) + if err != nil { + _ = cli.ShowAppHelp(c) + return cli.Exit(err, 1) + } + + results, err := faas.DeleteNamespace(cl, name).Extract() + if err != nil { + return cli.Exit(err, 1) + } + + return utils.WaitTaskAndShowResult(c, cl, results, false, func(task tasks.TaskID) (interface{}, error) { + _, err := faas.GetNamespace(cl, name).Extract() + if err != nil { + return nil, fmt.Errorf("cannot delete namespace with Name: %s", name) + } + + switch err.(type) { + case gcorecloud.ErrDefault404: + return nil, nil + default: + return nil, err + } + }) +} diff --git a/cmd/commands.go b/cmd/commands.go index 549b5049..eca70d6a 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -5,10 +5,11 @@ import ( "os" "path/filepath" + "github.com/G-Core/gcorelabscloud-go/client/ais/v1/ais" "github.com/G-Core/gcorelabscloud-go/client/apitokens/v1/apitokens" "github.com/G-Core/gcorelabscloud-go/client/apptemplates/v1/apptemplates" + "github.com/G-Core/gcorelabscloud-go/client/faas/v1/functions" "github.com/G-Core/gcorelabscloud-go/client/file_shares/v1/file_shares" - "github.com/G-Core/gcorelabscloud-go/client/ais/v1/ais" "github.com/G-Core/gcorelabscloud-go/client/flags" "github.com/G-Core/gcorelabscloud-go/client/flavors/v1/flavors" "github.com/G-Core/gcorelabscloud-go/client/floatingips/v1/floatingips" @@ -77,6 +78,7 @@ var commands = []*cli.Command{ &apitokens.Commands, &file_shares.Commands, &ais.Commands, + &functions.Commands, } type clientCommands struct { diff --git a/gcore/faas/v1/faas/requests.go b/gcore/faas/v1/faas/requests.go index 037fc662..5011eb6c 100644 --- a/gcore/faas/v1/faas/requests.go +++ b/gcore/faas/v1/faas/requests.go @@ -163,15 +163,19 @@ type CreateFunctionOptsBuilder interface { // CreateFunctionOpts represents options used to create a function. type CreateFunctionOpts struct { - Name string `json:"name"` - Description string `json:"description"` - Envs map[string]string `json:"envs"` - Runtime string `json:"runtime"` - Timeout int `json:"timeout"` - Flavor string `json:"flavor"` - Autoscaling FunctionAutoscaling `json:"autoscaling"` - CodeText string `json:"code_text"` - MainMethod string `json:"main_method"` + Name string `json:"name"` + Description string `json:"description"` + Envs map[string]string `json:"envs"` + Runtime string `json:"runtime"` + Timeout int `json:"timeout"` + Flavor string `json:"flavor"` + Autoscaling FunctionAutoscaling `json:"autoscaling"` + CodeText string `json:"code_text"` + EnableApiKey *bool `json:"enable_api_key,omitempty"` + Keys []string `json:"keys,omitempty"` + Disabled *bool `json:"disabled,omitempty"` + MainMethod string `json:"main_method"` + Dependencies string `json:"dependencies,omitempty"` } // ToFunctionCreateMap builds a request body from CreateFunctionOpts. @@ -212,13 +216,17 @@ type UpdateFunctionOptsBuilder interface { // UpdateFunctionOpts represents options used to Update a function. type UpdateFunctionOpts struct { - Description string `json:"description,omitempty"` - Envs map[string]string `json:"envs,omitempty"` - Timeout int `json:"timeout,omitempty"` - Flavor string `json:"flavor,omitempty"` - Autoscaling *FunctionAutoscaling `json:"autoscaling,omitempty"` - CodeText string `json:"code_text,omitempty"` - MainMethod string `json:"main_method,omitempty"` + Description string `json:"description,omitempty"` + Envs map[string]string `json:"envs,omitempty"` + Timeout int `json:"timeout,omitempty"` + Flavor string `json:"flavor,omitempty"` + Autoscaling *FunctionAutoscaling `json:"autoscaling,omitempty"` + CodeText string `json:"code_text,omitempty"` + EnableApiKey *bool `json:"enable_api_key,omitempty"` + Keys *[]string `json:"keys,omitempty"` + Disabled *bool `json:"disabled,omitempty"` + Dependencies string `json:"dependencies,omitempty"` + MainMethod string `json:"main_method,omitempty"` } // ToFunctionUpdateMap builds a request body from UpdateFunctionOpts. @@ -237,3 +245,106 @@ func UpdateFunction(c *gcorecloud.ServiceClient, nsName, fName string, opts Upda _, r.Err = c.Patch(url, b, &r.Body, nil) return } + +// ListKeys returns a Pager which allows you to iterate over a collection of +// keys. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func ListKeys(c *gcorecloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := keysListURL(c) + if opts != nil { + query, err := opts.ToFaaSListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return KeyPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListKeysAll returns all keys. +func ListKeysAll(c *gcorecloud.ServiceClient, opts ListOptsBuilder) ([]Key, error) { + page, err := ListKeys(c, opts).AllPages() + if err != nil { + return nil, err + } + return ExtractKeys(page) +} + +// CreateKeyOptsBuilder allows extensions to add additional parameters to the request. +type CreateKeyOptsBuilder interface { + ToKeyCreateMap() (map[string]any, error) +} + +// CreateKeyOpts represents options used to create a key. +type CreateKeyOpts struct { + Name string `json:"name"` + Description string `json:"description"` + Expire *gcorecloud.JSONRFC3339ZZ `json:"expire,omitempty"` + Functions []KeysFunction `json:"functions,omitempty"` +} + +func (opts CreateKeyOpts) ToKeyCreateMap() (map[string]any, error) { + return gcorecloud.BuildRequestBody(opts, "") +} + +// CreateKey create FaaS key. +func CreateKey(c *gcorecloud.ServiceClient, opts CreateKeyOptsBuilder) (r Key, err error) { + url := keysCreateURL(c) + b, err := opts.ToKeyCreateMap() + if err != nil { + return Key{}, err + } + + _, err = c.Post(url, b, &r, nil) + + return +} + +// DeleteKey delete FaaS key. +func DeleteKey(c *gcorecloud.ServiceClient, kName string) error { + url := keyURL(c, kName) + _, err := c.Delete(url, nil) + + return err +} + +// GetKey get FaaS key. +func GetKey(c *gcorecloud.ServiceClient, kName string) (r KeyResult) { + url := keyURL(c, kName) + _, r.Err = c.Get(url, &r.Body, nil) + + return +} + +// UpdateKeyOptsBuilder allows extensions to add additional parameters to the request. +type UpdateKeyOptsBuilder interface { + ToKeyUpdateMap() (map[string]any, error) +} + +// UpdateKeyOpts represents options used to Update a key. +type UpdateKeyOpts struct { + Description string `json:"description,omitempty"` + Expire *gcorecloud.JSONRFC3339ZZ `json:"expire,omitempty"` + Functions []KeysFunction `json:"functions,omitempty"` +} + +// ToKeyUpdateMap builds a request body from UpdateKeyOpts. +func (opts UpdateKeyOpts) ToKeyUpdateMap() (map[string]interface{}, error) { + return gcorecloud.BuildRequestBody(opts, "") +} + +// UpdateKey update FaaS key. +func UpdateKey(c *gcorecloud.ServiceClient, kName string, opts UpdateKeyOptsBuilder) (k Key, err error) { + url := keyURL(c, kName) + b, err := opts.ToKeyUpdateMap() + if err != nil { + return Key{}, err + } + + _, err = c.Patch(url, b, &k, nil) + + return +} diff --git a/gcore/faas/v1/faas/results.go b/gcore/faas/v1/faas/results.go index 27fa2298..44c13038 100644 --- a/gcore/faas/v1/faas/results.go +++ b/gcore/faas/v1/faas/results.go @@ -83,6 +83,7 @@ type Function struct { BuildStatus string `json:"build_status"` Status string `json:"status"` DeployStatus DeployStatus `json:"deploy_status"` + Dependencies string `json:"dependencies"` Envs map[string]string `json:"envs"` Runtime string `json:"runtime"` Timeout int `json:"timeout"` @@ -91,12 +92,15 @@ type Function struct { CodeText string `json:"code_text"` MainMethod string `json:"main_method"` Endpoint string `json:"endpoint"` + Disabled bool `json:"disabled"` + EnableAPIKey bool `json:"enable_api_key"` + Keys []string `json:"keys"` CreatedAt gcorecloud.JSONRFC3339ZZ `json:"created_at"` } type FunctionAutoscaling struct { - MinInstances int `json:"min_instances"` - MaxInstances int `json:"max_instances"` + MinInstances *int `json:"min_instances,omitempty"` + MaxInstances *int `json:"max_instances,omitempty"` } type FunctionResult struct { @@ -120,7 +124,7 @@ type FunctionPage struct { pagination.LinkedPageBase } -// NextPageURL is invoked when a paginated collection of namespaces has reached +// NextPageURL is invoked when a paginated collection of functions has reached // the end of a page and the pager seeks to traverse over a new one. In order // to do this, it needs to construct the next page's URL. func (f FunctionPage) NextPageURL() (string, error) { @@ -157,3 +161,73 @@ func ExtractFunctionsInto(p pagination.Page, v interface{}) error { type DeleteResult struct { gcorecloud.ErrResult } + +type KeysFunction struct { + Name string `json:"name"` + Namespace string `json:"namespace"` +} + +type Key struct { + Name string `json:"name"` + Description string `json:"description"` + Functions []KeysFunction `json:"functions"` + Expire gcorecloud.JSONRFC3339ZZ `json:"expire"` + CreatedAt gcorecloud.JSONRFC3339ZZ `json:"created_at"` + Secret string `json:"secret,omitempty"` + Status string `json:"status"` +} + +type KeyResult struct { + gcorecloud.Result +} + +func (r KeyResult) Extract() (*Key, error) { + var k Key + err := r.ExtractInto(&k) + + return &k, err +} + +func (r KeyResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "") +} + +// KeyPage is the page returned by a paper when traversing over a +// collection of keys +type KeyPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a pagination collection of keys has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (k KeyPage) NextPageURL() (string, error) { + var s struct { + Links []gcorecloud.Link `json:"links"` + } + err := k.ExtractInto(&s) + if err != nil { + return "", err + } + + return gcorecloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a Key struct is empty. +func (k KeyPage) IsEmpty() (bool, error) { + is, err := ExtractKeys(k) + return len(is) == 0, err +} + +// ExtractKeys accepts a Page struct, specifically a KeyPage struct, +// and extracts the elements into a slice of Key structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractKeys(p pagination.Page) ([]Key, error) { + var f []Key + err := ExtractKeysInto(p, &f) + return f, err +} + +func ExtractKeysInto(p pagination.Page, v interface{}) error { + return p.(KeyPage).Result.ExtractIntoSlicePtr(v, "results") +} diff --git a/gcore/faas/v1/faas/testing/fixtures.go b/gcore/faas/v1/faas/testing/fixtures.go index 18b03685..de9402aa 100644 --- a/gcore/faas/v1/faas/testing/fixtures.go +++ b/gcore/faas/v1/faas/testing/fixtures.go @@ -60,6 +60,9 @@ const listFunctionResponse = ` "ENV_VAR": "value 1", "ENVIRONMENT_VARIABLE": "value 2" }, + "enable_api_key": true, + "keys" :["key-one"], + "disabled": false, "runtime": "python3.7.12", "timeout": 5, "flavor": "64mCPU-64MB", @@ -82,6 +85,9 @@ const getFunctionResponse = ` "ENV_VAR": "value 1", "ENVIRONMENT_VARIABLE": "value 2" }, + "enable_api_key": true, + "keys" :["key-one"], + "disabled": false, "runtime": "python3.7.12", "timeout": 5, "flavor": "64mCPU-64MB", @@ -100,6 +106,8 @@ const createFunctionRequest = ` "envs": { "ENV_VAR": "value 1" }, + "enable_api_key": true, + "keys" :["key-one"], "runtime": "python3.7.12", "timeout": 5, "flavor": "64mCPU-64MB", @@ -128,6 +136,100 @@ const updateFunctionRequest = ` } ` +const getKeyResponse = ` +{ + "description": "description", + "name": "test-key", + "status": "active", + "functions": [ + { + "name": "function", + "namespace": "namespace" + } + ] +} +` + +const createKeyRequest = ` +{ + "description": "description", + "name": "test-key", + "functions": [ + { + "name": "function", + "namespace": "namespace" + } + ] +} +` + +const createKeyResponse = ` +{ + "description": "description", + "name": "test-key", + "status": "active", + "functions": [ + { + "name": "function", + "namespace": "namespace" + } + ] +} +` + +const listKeysResponse = ` +{ + "count": 1, + "results": [ + { + "description": "description", + "name": "test-key", + "status": "active", + "functions": [ + { + "name": "function", + "namespace": "namespace" + } + ] + } + ] +} +` + +const updateKeyRequest = ` +{ + "description": "long string", + "functions": [ + { + "name": "function1", + "namespace": "namespace1" + }, + { + "name": "function2", + "namespace": "namespace1" + } + ] +} +` + +const updateKeyResponse = ` +{ + "description": "long string", + "name": "test-key", + "status": "active", + "functions": [ + { + "name": "function1", + "namespace": "namespace1" + }, + { + "name": "function2", + "namespace": "namespace1" + } + ] +} +` + const taskResponse = ` { "tasks": [ @@ -151,6 +253,8 @@ var ( } fName = "function-name" + min = 1 + max = 2 expectedF = faas.Function{ Name: fName, Description: "Function description", @@ -162,11 +266,43 @@ var ( Timeout: 5, Flavor: "64mCPU-64MB", Autoscaling: faas.FunctionAutoscaling{ - MinInstances: 1, - MaxInstances: 2, + MinInstances: &min, + MaxInstances: &max, }, - CodeText: "def main(): print('It works!')", - MainMethod: "main", + CodeText: "def main(): print('It works!')", + MainMethod: "main", + EnableAPIKey: true, + Disabled: false, + Keys: []string{"key-one"}, } expectedFSlice = []faas.Function{expectedF} + + kName = "test-key" + expectedKey = faas.Key{ + Name: kName, + Description: "description", + Functions: []faas.KeysFunction{ + { + Name: "function", + Namespace: "namespace", + }, + }, + Status: "active", + } + expectedKeysSlice = []faas.Key{expectedKey} + expectedUpdatedKey = faas.Key{ + Name: kName, + Description: "long string", + Functions: []faas.KeysFunction{ + { + Name: "function1", + Namespace: "namespace1", + }, + { + Name: "function2", + Namespace: "namespace1", + }, + }, + Status: "active", + } ) diff --git a/gcore/faas/v1/faas/testing/requests_test.go b/gcore/faas/v1/faas/testing/requests_test.go index 13ee4b97..f7fce176 100644 --- a/gcore/faas/v1/faas/testing/requests_test.go +++ b/gcore/faas/v1/faas/testing/requests_test.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + "github.com/AlekSi/pointer" "github.com/G-Core/gcorelabscloud-go/gcore/faas/v1/faas" "github.com/G-Core/gcorelabscloud-go/pagination" fake "github.com/G-Core/gcorelabscloud-go/testhelper/client" @@ -55,6 +56,22 @@ func prepareUpdateFunctionTestURL(nsName, fName string) string { return prepareFunctionTestURLParams(fake.ProjectID, fake.RegionID, nsName, fName) } +func prepareKeyTestURL() string { + return fmt.Sprintf("/v1/faas/keys/%d/%d", fake.ProjectID, fake.RegionID) +} + +func prepareGetKeyTestURL(kName string) string { + return fmt.Sprintf("/v1/faas/keys/%d/%d/%s", fake.ProjectID, fake.RegionID, kName) +} + +func prepareDeleteKeyTestURL(kName string) string { + return fmt.Sprintf("/v1/faas/keys/%d/%d/%s", fake.ProjectID, fake.RegionID, kName) +} + +func prepareUpdateKeyTestURL(kName string) string { + return fmt.Sprintf("/v1/faas/keys/%d/%d/%s", fake.ProjectID, fake.RegionID, kName) +} + func TestGetNamespace(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() @@ -331,11 +348,13 @@ func TestCreateFunction(t *testing.T) { Timeout: 5, Flavor: "64mCPU-64MB", Autoscaling: faas.FunctionAutoscaling{ - MinInstances: 1, - MaxInstances: 2, + MinInstances: pointer.To(1), + MaxInstances: pointer.To(2), }, - CodeText: "def main(): print('It works!')", - MainMethod: "main", + EnableApiKey: pointer.To(true), + Keys: []string{"key-one"}, + CodeText: "def main(): print('It works!')", + MainMethod: "main", } task, err := faas.CreateFunction(client, nsName, opts).Extract() require.NoError(t, err) @@ -392,8 +411,8 @@ func TestUpdateFunction(t *testing.T) { Timeout: 180, Flavor: "string", Autoscaling: &faas.FunctionAutoscaling{ - MinInstances: 1, - MaxInstances: 2, + MinInstances: pointer.To(1), + MaxInstances: pointer.To(2), }, CodeText: "string", MainMethod: "string", @@ -402,3 +421,176 @@ func TestUpdateFunction(t *testing.T) { require.NoError(t, err) require.Equal(t, tasks1, *task) } + +func TestGetKey(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareGetKeyTestURL(kName), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, getKeyResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("faas/keys", "v1") + key, err := faas.GetKey(client, kName).Extract() + require.NoError(t, err) + require.Equal(t, expectedKey, *key) +} + +func TestListKeys(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareKeyTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, listKeysResponse) + if err != nil { + log.Error(err) + } + }) + + var count int + client := fake.ServiceTokenClient("faas/keys", "v1") + err := faas.ListKeys(client, nil).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := faas.ExtractKeys(page) + require.NoError(t, err) + ct := actual[0] + require.Equal(t, expectedKey, ct) + require.Equal(t, expectedKeysSlice, actual) + return true, nil + }) + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestListAllKeys(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareKeyTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err := fmt.Fprint(w, listKeysResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("faas/keys", "v1") + actual, err := faas.ListKeysAll(client, nil) + require.NoError(t, err) + + ct := actual[0] + require.Equal(t, expectedKey, ct) + require.Equal(t, expectedKeysSlice, actual) + +} + +func TestCreateKeys(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareKeyTestURL(), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createKeyRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, createKeyResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("faas/keys", "v1") + opts := faas.CreateKeyOpts{ + Name: "test-key", + Description: "description", + Functions: []faas.KeysFunction{ + { + Name: "function", + Namespace: "namespace", + }, + }, + } + key, err := faas.CreateKey(client, &opts) + require.NoError(t, err) + require.Equal(t, expectedKey, key) +} + +func TestDeleteKeys(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareDeleteKeyTestURL(nsName), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + }) + + client := fake.ServiceTokenClient("faas/keys", "v1") + err := faas.DeleteKey(client, nsName) + require.NoError(t, err) +} + +func TestUpdateKeys(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(prepareUpdateKeyTestURL(nsName), func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PATCH") + th.TestHeader(t, r, "Authorization", fmt.Sprintf("Bearer %s", fake.AccessToken)) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateKeyRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + _, err := fmt.Fprint(w, updateKeyResponse) + if err != nil { + log.Error(err) + } + }) + + client := fake.ServiceTokenClient("faas/keys", "v1") + opts := faas.UpdateKeyOpts{ + Description: "long string", + Functions: []faas.KeysFunction{ + { + Name: "function1", + Namespace: "namespace1", + }, + { + Name: "function2", + Namespace: "namespace1", + }, + }, + } + key, err := faas.UpdateKey(client, nsName, opts) + require.NoError(t, err) + require.Equal(t, expectedUpdatedKey, key) +} diff --git a/gcore/faas/v1/faas/urls.go b/gcore/faas/v1/faas/urls.go index e0ba6456..29c9c651 100644 --- a/gcore/faas/v1/faas/urls.go +++ b/gcore/faas/v1/faas/urls.go @@ -31,3 +31,15 @@ func functionCreateURL(c *gcorecloud.ServiceClient, namespaceName string) string func functionURL(c *gcorecloud.ServiceClient, namespaceName, functionName string) string { return c.ServiceURL(namespaceName, "functions", functionName) } + +func keysListURL(c *gcorecloud.ServiceClient) string { + return rootURL(c) +} + +func keysCreateURL(c *gcorecloud.ServiceClient) string { + return rootURL(c) +} + +func keyURL(c *gcorecloud.ServiceClient, keyName string) string { + return c.ServiceURL(keyName) +} diff --git a/go.mod b/go.mod index d0b3daf1..15cd5c4e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/G-Core/gcorelabscloud-go -go 1.17 +go 1.21 require ( github.com/fatih/structs v1.1.0 @@ -19,7 +19,7 @@ require ( github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc github.com/sirupsen/logrus v1.6.0 github.com/stretchr/testify v1.4.0 - github.com/urfave/cli/v2 v2.1.1 + github.com/urfave/cli/v2 v2.25.7 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/appengine v1.6.6 // indirect @@ -29,8 +29,10 @@ require ( k8s.io/client-go v0.18.2 ) +require github.com/AlekSi/pointer v1.2.0 + require ( - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gofuzz v1.1.0 // indirect @@ -40,9 +42,9 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect diff --git a/go.sum b/go.sum index 345452af..dbe92c00 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= @@ -14,8 +16,8 @@ github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -132,14 +134,12 @@ github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -151,8 +151,10 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= -github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=