Add users CLI command and do some cleanup (#269)

This commit is contained in:
Jeff Mitchell authored and GitHub committed 2020-08-13 20:59:32 -04:00
1 parent 6080d93f8f
commit efaf58b568
11 files changed
+399 -66

No files matched your search

+37
View File
@@ -15,6 +15,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/commands/hosts"
"github.com/hashicorp/boundary/internal/cmd/commands/roles"
"github.com/hashicorp/boundary/internal/cmd/commands/scopes"
"github.com/hashicorp/boundary/internal/cmd/commands/users"
"github.com/hashicorp/boundary/internal/cmd/commands/worker"
"github.com/mitchellh/cli"
@@ -275,6 +276,42 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Func: "list",
}, nil
},
"users": func() (cli.Command, error) {
return &users.Command{
Command: base.NewCommand(ui),
}, nil
},
"users create": func() (cli.Command, error) {
return &users.Command{
Command: base.NewCommand(ui),
Func: "create",
}, nil
},
"users read": func() (cli.Command, error) {
return &users.Command{
Command: base.NewCommand(ui),
Func: "read",
}, nil
},
"users update": func() (cli.Command, error) {
return &users.Command{
Command: base.NewCommand(ui),
Func: "update",
}, nil
},
"users delete": func() (cli.Command, error) {
return &users.Command{
Command: base.NewCommand(ui),
Func: "delete",
}, nil
},
"users list": func() (cli.Command, error) {
return &users.Command{
Command: base.NewCommand(ui),
Func: "list",
}, nil
},
}
}
@@ -34,7 +34,7 @@ var flagsMap = map[string][]string{
}
func (c *Command) Help() string {
helpMap := common.HelpMap(resource.AuthToken)
helpMap := common.HelpMap("auth-token")
if c.Func == "" {
return helpMap["base"]()
}
@@ -44,9 +44,11 @@ func (c *Command) Help() string {
func (c *Command) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
if len(flagsMap[c.Func]) > 0 {
f := set.NewFlagSet("Command Options")
common.PopulateCommonFlags(c.Command, f, resource.AuthToken, flagsMap[c.Func])
}
common.PopulateCommonFlags(c.Command, f, resource.AuthToken, flagsMap[c.Func])
return set
}
@@ -136,7 +138,7 @@ func (c *Command) Run(args []string) int {
}
b, err := base.JsonFormatter{}.Format(listedTokens)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
@@ -176,7 +178,7 @@ func (c *Command) Run(args []string) int {
case "json":
b, err := base.JsonFormatter{}.Format(token)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
+9 -9
View File
@@ -76,31 +76,31 @@ func generateGroupTableOutput(group *groups.Group) string {
output = []string{
"",
"Group information:",
fmt.Sprintf(" ID: %s", group.Id),
fmt.Sprintf(" Created At: %s", group.CreatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Updated At: %s", group.UpdatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Version: %d", group.Version),
fmt.Sprintf(" ID: %s", group.Id),
fmt.Sprintf(" Version: %d", group.Version),
fmt.Sprintf(" Created At: %s", group.CreatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Updated At: %s", group.UpdatedTime.Local().Format(time.RFC3339)),
}
}
if group.Name != "" {
output = append(output,
fmt.Sprintf(" Name: %s", group.Name),
fmt.Sprintf(" Name: %s", group.Name),
)
}
if group.Description != "" {
output = append(output,
fmt.Sprintf(" Description: %s", group.Description),
fmt.Sprintf(" Description: %s", group.Description),
)
}
if len(group.Members) > 0 {
output = append(output,
fmt.Sprintf(" Members: %s", ""),
fmt.Sprintf(" Members: %s", ""),
)
}
for _, member := range group.Members {
output = append(output,
fmt.Sprintf(" ID: %s", member.Id),
fmt.Sprintf(" Scope ID: %s", member.ScopeId),
fmt.Sprintf(" ID: %s", member.Id),
fmt.Sprintf(" Scope ID: %s", member.ScopeId),
)
}
return base.WrapForHelpText(output)
+11 -9
View File
@@ -7,7 +7,6 @@ import (
"github.com/hashicorp/boundary/api/groups"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/kr/pretty"
"github.com/mitchellh/cli"
@@ -36,7 +35,7 @@ func (c *Command) Synopsis() string {
}
var helpMap = func() map[string]func() string {
ret := common.HelpMap(resource.Group)
ret := common.HelpMap("group")
ret["add-members"] = addMembersHelp
ret["set-members"] = setMembersHelp
ret["remove-members"] = removeMembersHelp
@@ -64,9 +63,11 @@ func (c *Command) Help() string {
func (c *Command) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
if len(flagsMap[c.Func]) > 0 {
f := set.NewFlagSet("Command Options")
populateFlags(c, f, flagsMap[c.Func])
}
populateFlags(c, f, flagsMap[c.Func])
return set
}
@@ -219,7 +220,7 @@ func (c *Command) Run(args []string) int {
}
b, err := base.JsonFormatter{}.Format(listedGroups)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
@@ -240,17 +241,18 @@ func (c *Command) Run(args []string) int {
}
if true {
output = append(output,
fmt.Sprintf(" ID: %s", g.Id),
fmt.Sprintf(" ID: %s", g.Id),
fmt.Sprintf(" Version: %d", g.Version),
)
}
if g.Name != "" {
output = append(output,
fmt.Sprintf(" Name: %s", g.Name),
fmt.Sprintf(" Name: %s", g.Name),
)
}
if g.Description != "" {
output = append(output,
fmt.Sprintf(" Description: %s", g.Description),
fmt.Sprintf(" Description: %s", g.Description),
)
}
}
@@ -265,7 +267,7 @@ func (c *Command) Run(args []string) int {
case "json":
b, err := base.JsonFormatter{}.Format(group)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
+1 -1
View File
@@ -93,9 +93,9 @@ func generateRoleTableOutput(role *roles.Role) string {
"",
"Role information:",
fmt.Sprintf(" ID: %s", role.Id),
fmt.Sprintf(" Version: %d", role.Version),
fmt.Sprintf(" Created At: %s", role.CreatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Updated At: %s", role.UpdatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Version: %d", role.Version),
}
}
if role.Name != "" {
+11 -10
View File
@@ -8,7 +8,6 @@ import (
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/kr/pretty"
"github.com/mitchellh/cli"
@@ -41,7 +40,7 @@ func (c *Command) Synopsis() string {
}
var helpMap = func() map[string]func() string {
ret := common.HelpMap(resource.Role)
ret := common.HelpMap("role")
ret["add-principals"] = addPrincipalsHelp
ret["set-principals"] = setPrincipalsHelp
ret["remove-principals"] = removePrincipalsHelp
@@ -75,9 +74,10 @@ func (c *Command) Help() string {
func (c *Command) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
populateFlags(c, f, flagsMap[c.Func])
if len(flagsMap[c.Func]) > 0 {
f := set.NewFlagSet("Command Options")
populateFlags(c, f, flagsMap[c.Func])
}
return set
}
@@ -273,7 +273,7 @@ func (c *Command) Run(args []string) int {
}
b, err := base.JsonFormatter{}.Format(listedRoles)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
@@ -294,17 +294,18 @@ func (c *Command) Run(args []string) int {
}
if true {
output = append(output,
fmt.Sprintf(" ID: %s", r.Id),
fmt.Sprintf(" ID: %s", r.Id),
fmt.Sprintf(" Version: %d", r.Version),
)
}
if r.Name != "" {
output = append(output,
fmt.Sprintf(" Name: %s", r.Name),
fmt.Sprintf(" Name: %s", r.Name),
)
}
if r.Description != "" {
output = append(output,
fmt.Sprintf(" Description: %s", r.Description),
fmt.Sprintf(" Description: %s", r.Description),
)
}
}
@@ -319,7 +320,7 @@ func (c *Command) Run(args []string) int {
case "json":
b, err := base.JsonFormatter{}.Format(role)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
+1
View File
@@ -16,6 +16,7 @@ func generateScopeTableOutput(in *scopes.Scope) string {
"",
"Scope information:",
fmt.Sprintf(" ID: %s", in.Id),
fmt.Sprintf(" Version: %d", in.Version),
fmt.Sprintf(" Created Time: %s", in.CreatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Updated Time: %s", in.UpdatedTime.Local().Format(time.RFC3339)),
}...,
+11 -8
View File
@@ -35,7 +35,7 @@ var flagsMap = map[string][]string{
}
func (c *Command) Help() string {
helpMap := common.HelpMap(resource.Scope)
helpMap := common.HelpMap("scope")
if c.Func == "" {
return helpMap["base"]()
}
@@ -45,9 +45,11 @@ func (c *Command) Help() string {
func (c *Command) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
if len(flagsMap[c.Func]) > 0 {
f := set.NewFlagSet("Command Options")
common.PopulateCommonFlags(c.Command, f, resource.Scope, flagsMap[c.Func])
}
common.PopulateCommonFlags(c.Command, f, resource.Scope, flagsMap[c.Func])
return set
}
@@ -173,7 +175,7 @@ func (c *Command) Run(args []string) int {
}
b, err := base.JsonFormatter{}.Format(listedScopes)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
@@ -194,17 +196,18 @@ func (c *Command) Run(args []string) int {
}
if true {
output = append(output,
fmt.Sprintf(" ID: %s", s.Id),
fmt.Sprintf(" ID: %s", s.Id),
fmt.Sprintf(" Version: %d", s.Version),
)
}
if s.Name != "" {
output = append(output,
fmt.Sprintf(" Name: %s", s.Name),
fmt.Sprintf(" Name: %s", s.Name),
)
}
if s.Description != "" {
output = append(output,
fmt.Sprintf(" Description: %s", s.Description),
fmt.Sprintf(" Description: %s", s.Description),
)
}
}
@@ -219,7 +222,7 @@ func (c *Command) Run(args []string) int {
case "json":
b, err := base.JsonFormatter{}.Format(scope)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting to JSON: %w", err).Error())
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
+37
View File
@@ -0,0 +1,37 @@
package users
import (
"fmt"
"time"
"github.com/hashicorp/boundary/api/users"
"github.com/hashicorp/boundary/internal/cmd/base"
)
func generateUserTableOutput(in *users.User) string {
var ret []string
// This if true is here to line up columns for easy editing
if true {
ret = append(ret, []string{
"",
"User information:",
fmt.Sprintf(" ID: %s", in.Id),
fmt.Sprintf(" Version: %d", in.Version),
fmt.Sprintf(" Created Time: %s", in.CreatedTime.Local().Format(time.RFC3339)),
fmt.Sprintf(" Updated Time: %s", in.UpdatedTime.Local().Format(time.RFC3339)),
}...,
)
}
if in.Name != "" {
ret = append(ret,
fmt.Sprintf(" Name: %s", in.Name),
)
}
if in.Description != "" {
ret = append(ret,
fmt.Sprintf(" Description: %s", in.Description),
)
}
return base.WrapForHelpText(ret)
}
+232
View File
@@ -0,0 +1,232 @@
package users
import (
"fmt"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/users"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/kr/pretty"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*Command)(nil)
var _ cli.CommandAutocomplete = (*Command)(nil)
type Command struct {
*base.Command
Func string
}
func (c *Command) Synopsis() string {
return common.SynopsisFunc(c.Func, "user")
}
var flagsMap = map[string][]string{
"create": {"name", "description"},
"update": {"id", "name", "description", "version"},
"read": {"id"},
"delete": {"id"},
}
func (c *Command) Help() string {
helpMap := common.HelpMap("user")
if c.Func == "" {
return helpMap["base"]()
}
return helpMap[c.Func]() + "\n\n" + c.Flags().Help()
}
func (c *Command) Flags() *base.FlagSets {
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
if len(flagsMap[c.Func]) > 0 {
f := set.NewFlagSet("Command Options")
common.PopulateCommonFlags(c.Command, f, resource.User, flagsMap[c.Func])
}
return set
}
func (c *Command) AutocompleteArgs() complete.Predictor {
return complete.PredictAnything
}
func (c *Command) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *Command) Run(args []string) int {
if c.Func == "" {
return cli.RunResultHelp
}
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
if strutil.StrListContains(flagsMap[c.Func], "id") && c.FlagId == "" {
c.UI.Error("ID is required but not passed in via -id")
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(fmt.Sprintf("Error creating API client: %s", err.Error()))
return 2
}
var opts []users.Option
switch c.FlagName {
case "":
case "null":
opts = append(opts, users.DefaultName())
default:
opts = append(opts, users.WithName(c.FlagName))
}
switch c.FlagDescription {
case "":
case "null":
opts = append(opts, users.DefaultDescription())
default:
opts = append(opts, users.WithDescription(c.FlagDescription))
}
userClient := users.NewUsersClient(client)
// Perform check-and-set when needed
var version uint32
switch c.Func {
case "create", "read", "delete", "list":
// These don't udpate so don't need the existing version
default:
switch c.FlagVersion {
case 0:
opts = append(opts, users.WithAutomaticVersioning())
default:
version = uint32(c.FlagVersion)
}
}
var existed bool
var user *users.User
var listedUsers []*users.User
var apiErr *api.Error
switch c.Func {
case "create":
user, apiErr, err = userClient.Create(c.Context, opts...)
case "update":
user, apiErr, err = userClient.Update(c.Context, c.FlagId, version, opts...)
case "read":
user, apiErr, err = userClient.Read(c.Context, c.FlagId, opts...)
case "delete":
existed, apiErr, err = userClient.Delete(c.Context, c.FlagId, opts...)
case "list":
listedUsers, apiErr, err = userClient.List(c.Context, opts...)
}
plural := "user"
if c.Func == "list" {
plural = "users"
}
if err != nil {
c.UI.Error(fmt.Sprintf("Error trying to %s %s: %s", c.Func, plural, err.Error()))
return 2
}
if apiErr != nil {
c.UI.Error(fmt.Sprintf("Error from controller when performing %s on %s: %s", c.Func, plural, pretty.Sprint(apiErr)))
return 1
}
switch c.Func {
case "delete":
switch base.Format(c.UI) {
case "json":
c.UI.Output("null")
case "table":
output := "The delete operation completed successfully"
switch existed {
case true:
output += "."
default:
output += ", however the resource did not exist at the time."
}
c.UI.Output(output)
}
return 0
case "list":
switch base.Format(c.UI) {
case "json":
if len(listedUsers) == 0 {
c.UI.Output("null")
return 0
}
b, err := base.JsonFormatter{}.Format(listedUsers)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
case "table":
if len(listedUsers) == 0 {
c.UI.Output("No users found")
return 0
}
var output []string
output = []string{
"",
"User information:",
}
for i, u := range listedUsers {
if i > 0 {
output = append(output, "")
}
if true {
output = append(output,
fmt.Sprintf(" ID: %s", u.Id),
fmt.Sprintf(" Version: %d", u.Version),
)
}
if u.Name != "" {
output = append(output,
fmt.Sprintf(" Name: %s", u.Name),
)
}
if u.Description != "" {
output = append(output,
fmt.Sprintf(" Description: %s", u.Description),
)
}
}
c.UI.Output(base.WrapForHelpText(output))
}
return 0
}
switch base.Format(c.UI) {
case "table":
c.UI.Output(generateUserTableOutput(user))
case "json":
b, err := base.JsonFormatter{}.Format(user)
if err != nil {
c.UI.Error(fmt.Errorf("Error formatting as JSON: %w", err).Error())
return 1
}
c.UI.Output(string(b))
}
return 0
}
+42 -24
View File
@@ -10,75 +10,84 @@ import (
"github.com/mitchellh/go-wordwrap"
)
func SynopsisFunc(inFunc, resourceType string) string {
func SynopsisFunc(inFunc, resType string) string {
if inFunc == "" {
return wordwrap.WrapString(fmt.Sprintf("Manage Boundary %ss", resourceType), base.TermWidth)
return wordwrap.WrapString(fmt.Sprintf("Manage Boundary %ss", resType), base.TermWidth)
}
return wordwrap.WrapString(fmt.Sprintf("%s a %s within Boundary", textproto.CanonicalMIMEHeaderKey(inFunc), resourceType), base.TermWidth)
articleType := resType
switch resType[0] {
case 'a', 'e', 'i', 'o':
articleType = fmt.Sprintf("an %s", articleType)
default:
articleType = fmt.Sprintf("a %s", articleType)
}
return wordwrap.WrapString(fmt.Sprintf("%s %s within Boundary", textproto.CanonicalMIMEHeaderKey(inFunc), articleType), base.TermWidth)
}
func HelpMap(resourceType resource.Type) map[string]func() string {
func HelpMap(resType string) map[string]func() string {
prefixMap := map[string]string{
resource.Scope.String(): "o",
resource.Role.String(): "r",
resource.Group.String(): "g",
resource.User.String(): "u",
resource.Scope.String(): "o",
resource.AuthToken.String(): "at",
resource.AuthMethod.String(): "am",
resource.Role.String(): "r",
resource.Group.String(): "g",
resource.User.String(): "u",
}
return map[string]func() string{
"base": func() string {
return base.WrapForHelpText(subType([]string{
"Usage: boundary {{type}}s [sub command] [options] [args]",
"",
" This command allows operations on Boundary {{type}}s. Examples",
" This command allows operations on Boundary {{type}} resources. Examples",
"",
" Create a {{type}}:",
" Create {{articletype}}:",
"",
` $ boundary {{type}}s create -name prodops -description "For ProdOps usage"`,
"",
" Please see the {{type}}s subcommand help for detailed usage information.",
}, resourceType, prefixMap))
}, resType, prefixMap))
},
"create": func() string {
return base.WrapForHelpText(subType([]string{
"Usage: boundary {{type}}s create [options] [args]",
"",
" Create a {{type}}. Example:",
" Create {{articletype}}. Example:",
"",
` $ boundary {{type}}s create -name prodops -description "{{uppertype}} for ProdOps"`,
"",
"",
}, resourceType, prefixMap))
}, resType, prefixMap))
},
"update": func() string {
return base.WrapForHelpText(subType([]string{
"Usage: boundary {{type}}s update [options] [args]",
"",
" Update a {{type}} given its ID. Example:",
" Update {{articletype}} given its ID. Example:",
"",
` $ boundary {{type}}s update -id {{prefix}}_1234567890 -name "devops" -description "{{uppertype}} for DevOps"`,
}, resourceType, prefixMap))
}, resType, prefixMap))
},
"read": func() string {
return base.WrapForHelpText(subType([]string{
"Usage: boundary {{type}}s read [options] [args]",
"",
" Read a {{type}} given its ID. Example:",
" Read {{articletype}} given its ID. Example:",
"",
` $ boundary {{type}}s read -id {{prefix}}_1234567890`,
}, resourceType, prefixMap))
}, resType, prefixMap))
},
"delete": func() string {
return base.WrapForHelpText(subType([]string{
"Usage: boundary {{type}}s delete [options] [args]",
"",
" Delete a {{type}} given its ID. Example:",
" Delete {{articletype}} given its ID. Example:",
"",
` $ boundary {{type}}s delete -id {{prefix}}_1234567890`,
}, resourceType, prefixMap))
}, resType, prefixMap))
},
"list": func() string {
@@ -88,20 +97,29 @@ func HelpMap(resourceType resource.Type) map[string]func() string {
" List {{type}}s within an enclosing scope or resource. Example:",
"",
` $ boundary {{type}}s list`,
}, resourceType, prefixMap))
}, resType, prefixMap))
},
}
}
func subType(in []string, resType resource.Type, prefixMap map[string]string) []string {
func subType(in []string, resType string, prefixMap map[string]string) []string {
articleType := resType
switch resType[0] {
case 'a', 'e', 'i', 'o':
articleType = fmt.Sprintf("an %s", articleType)
default:
articleType = fmt.Sprintf("a %s", articleType)
}
for i, v := range in {
in[i] =
strings.Replace(
strings.Replace(
strings.Replace(
v, "{{type}}", resType.String(), -1),
"{{uppertype}}", textproto.CanonicalMIMEHeaderKey(resType.String()), -1),
"{{prefix}}", prefixMap[resType.String()], -1)
strings.Replace(
v, "{{type}}", resType, -1),
"{{uppertype}}", textproto.CanonicalMIMEHeaderKey(resType), -1),
"{{prefix}}", prefixMap[resType], -1),
"{{articletype}}", articleType, -1)
}
return in
}