diff --git a/weed/command/shell.go b/weed/command/shell.go index 962fe26ee..4053646ad 100644 --- a/weed/command/shell.go +++ b/weed/command/shell.go @@ -2,6 +2,7 @@ package command import ( "fmt" + "os" "github.com/seaweedfs/seaweedfs/weed/pb" @@ -60,7 +61,7 @@ func runShell(command *Command, args []string) bool { filerAddress = viper.GetString("cluster." + cluster + ".filer") } shellOptions.FilerAddress = pb.ServerAddress(filerAddress) - fmt.Printf("master: %s filer: %s\n", *shellOptions.Masters, shellOptions.FilerAddress) + fmt.Fprintf(os.Stderr, "master: %s filer: %s\n", *shellOptions.Masters, shellOptions.FilerAddress) shell.RunShell(shellOptions) diff --git a/weed/shell/command_s3_policy_attach.go b/weed/shell/command_s3_policy_attach.go index b170d6e56..a9883edfa 100644 --- a/weed/shell/command_s3_policy_attach.go +++ b/weed/shell/command_s3_policy_attach.go @@ -2,6 +2,7 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" @@ -74,8 +75,7 @@ func (c *commandS3PolicyAttach) Do(args []string, commandEnv *CommandEnv, writer // Check if already attached for _, p := range resp.Identity.PolicyNames { if p == *policy { - fmt.Fprintf(writer, "Policy %q is already attached to user %q.\n", *policy, *user) - return nil + return json.NewEncoder(writer).Encode(map[string]string{"policy": *policy, "user": *user}) } } @@ -88,7 +88,6 @@ func (c *commandS3PolicyAttach) Do(args []string, commandEnv *CommandEnv, writer return err } - fmt.Fprintf(writer, "Attached policy %q to user %q\n", *policy, *user) - return nil + return json.NewEncoder(writer).Encode(map[string]string{"policy": *policy, "user": *user}) }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) } diff --git a/weed/shell/command_s3_policy_detach.go b/weed/shell/command_s3_policy_detach.go index d66896f29..26971ab9b 100644 --- a/weed/shell/command_s3_policy_detach.go +++ b/weed/shell/command_s3_policy_detach.go @@ -2,6 +2,7 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" @@ -84,7 +85,6 @@ func (c *commandS3PolicyDetach) Do(args []string, commandEnv *CommandEnv, writer return err } - fmt.Fprintf(writer, "Detached policy %q from user %q\n", *policy, *user) - return nil + return json.NewEncoder(writer).Encode(map[string]string{"policy": *policy, "user": *user}) }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) } diff --git a/weed/shell/command_s3_user_create.go b/weed/shell/command_s3_user_create.go index c9a602213..8fd056ab6 100644 --- a/weed/shell/command_s3_user_create.go +++ b/weed/shell/command_s3_user_create.go @@ -2,9 +2,11 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" + "os" "time" "github.com/seaweedfs/seaweedfs/weed/iam" @@ -34,6 +36,8 @@ func (c *commandS3UserCreate) Help() string { are omitted, they are generated automatically. After creating a user, attach policies with s3.policy.attach. + + Output: JSON to stdout. Secret key is printed to stderr only. ` } @@ -56,8 +60,10 @@ func (c *commandS3UserCreate) Do(args []string, commandEnv *CommandEnv, writer i ak := *accessKey sk := *secretKey + generated := false if ak == "" && sk == "" { + generated = true var err error ak, err = iam.GenerateRandomString(iam.AccessKeyIdLength, iam.CharsetUpper) if err != nil { @@ -93,10 +99,13 @@ func (c *commandS3UserCreate) Do(args []string, commandEnv *CommandEnv, writer i return err } - fmt.Fprintf(writer, "Created user %q\n", *name) - fmt.Fprintf(writer, "Access Key: %s\n", ak) - fmt.Fprintf(writer, "Secret Key: %s\n", sk) - fmt.Fprintln(writer) - fmt.Fprintln(writer, "Save these credentials - the secret key cannot be retrieved later.") - return nil + if generated { + fmt.Fprintf(os.Stderr, "Secret Key: %s\n", sk) + fmt.Fprintf(os.Stderr, "Save this secret key - it cannot be retrieved later.\n") + } + + return json.NewEncoder(writer).Encode(map[string]string{ + "name": *name, + "access_key": ak, + }) } diff --git a/weed/shell/command_s3_user_delete.go b/weed/shell/command_s3_user_delete.go index b74435888..6d4698b1c 100644 --- a/weed/shell/command_s3_user_delete.go +++ b/weed/shell/command_s3_user_delete.go @@ -2,6 +2,7 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" @@ -56,6 +57,5 @@ func (c *commandS3UserDelete) Do(args []string, commandEnv *CommandEnv, writer i return err } - fmt.Fprintf(writer, "Deleted user %q\n", *name) - return nil + return json.NewEncoder(writer).Encode(map[string]string{"name": *name}) } diff --git a/weed/shell/command_s3_user_disable.go b/weed/shell/command_s3_user_disable.go index 3799460be..34af926fe 100644 --- a/weed/shell/command_s3_user_disable.go +++ b/weed/shell/command_s3_user_disable.go @@ -2,6 +2,7 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" @@ -62,7 +63,6 @@ func (c *commandS3UserDisable) Do(args []string, commandEnv *CommandEnv, writer } if resp.Identity.Disabled { - fmt.Fprintf(writer, "User %q is already disabled.\n", *name) return nil } @@ -77,6 +77,5 @@ func (c *commandS3UserDisable) Do(args []string, commandEnv *CommandEnv, writer return err } - fmt.Fprintf(writer, "Disabled user %q\n", *name) - return nil + return json.NewEncoder(writer).Encode(map[string]string{"name": *name, "status": "disabled"}) } diff --git a/weed/shell/command_s3_user_enable.go b/weed/shell/command_s3_user_enable.go index 76a3635ed..3040db2c3 100644 --- a/weed/shell/command_s3_user_enable.go +++ b/weed/shell/command_s3_user_enable.go @@ -2,6 +2,7 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" @@ -58,22 +59,19 @@ func (c *commandS3UserEnable) Do(args []string, commandEnv *CommandEnv, writer i return fmt.Errorf("user %q returned empty identity", *name) } - if !resp.Identity.Disabled { - fmt.Fprintf(writer, "User %q is already enabled.\n", *name) - return nil + if resp.Identity.Disabled { + resp.Identity.Disabled = false + _, err = client.UpdateUser(ctx, &iam_pb.UpdateUserRequest{ + Username: *name, + Identity: resp.Identity, + }) + return err } - - resp.Identity.Disabled = false - _, err = client.UpdateUser(ctx, &iam_pb.UpdateUserRequest{ - Username: *name, - Identity: resp.Identity, - }) - return err + return nil }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) if err != nil { return err } - fmt.Fprintf(writer, "Enabled user %q\n", *name) - return nil + return json.NewEncoder(writer).Encode(map[string]string{"name": *name, "status": "enabled"}) } diff --git a/weed/shell/command_s3_user_list.go b/weed/shell/command_s3_user_list.go index 7955b3e1a..f0107d536 100644 --- a/weed/shell/command_s3_user_list.go +++ b/weed/shell/command_s3_user_list.go @@ -2,10 +2,9 @@ package shell import ( "context" - "fmt" + "encoding/json" "io" "strings" - "text/tabwriter" "time" "github.com/seaweedfs/seaweedfs/weed/pb" @@ -29,7 +28,7 @@ func (c *commandS3UserList) Help() string { s3.user.list - Lists all users with their status, attached policies, and credential count. + Output: JSON array of users with status, policies, and credential count. ` } @@ -37,6 +36,13 @@ func (c *commandS3UserList) HasTag(CommandTag) bool { return false } +type s3UserListEntry struct { + Name string `json:"name"` + Status string `json:"status"` + Policies []string `json:"policies"` + Keys int `json:"keys"` +} + func (c *commandS3UserList) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { return pb.WithGrpcClient(false, 0, func(conn *grpc.ClientConn) error { client := iam_pb.NewSeaweedIdentityAccessManagementClient(conn) @@ -48,27 +54,27 @@ func (c *commandS3UserList) Do(args []string, commandEnv *CommandEnv, writer io. return err } - identities := resp.Configuration.GetIdentities() - if len(identities) == 0 { - fmt.Fprintln(writer, "No users found.") - return nil - } - - tw := tabwriter.NewWriter(writer, 0, 4, 2, ' ', 0) - fmt.Fprintln(tw, "NAME\tSTATUS\tPOLICIES\tKEYS") - - for _, id := range identities { + var result []s3UserListEntry + for _, id := range resp.Configuration.GetIdentities() { status := "enabled" if id.Disabled { status = "disabled" } - policies := "-" - if len(id.PolicyNames) > 0 { - policies = joinMax(id.PolicyNames, 3) + policies := id.PolicyNames + if policies == nil { + policies = []string{} } - fmt.Fprintf(tw, "%s\t%s\t%s\t%d\n", id.Name, status, policies, len(id.Credentials)) + result = append(result, s3UserListEntry{ + Name: id.Name, + Status: status, + Policies: policies, + Keys: len(id.Credentials), + }) } - return tw.Flush() + if result == nil { + result = []s3UserListEntry{} + } + return json.NewEncoder(writer).Encode(result) }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) } diff --git a/weed/shell/command_s3_user_show.go b/weed/shell/command_s3_user_show.go index 1841ab1b9..92ba126cd 100644 --- a/weed/shell/command_s3_user_show.go +++ b/weed/shell/command_s3_user_show.go @@ -2,10 +2,10 @@ package shell import ( "context" + "encoding/json" "flag" "fmt" "io" - "strings" "time" "github.com/seaweedfs/seaweedfs/weed/pb" @@ -28,6 +28,8 @@ func (c *commandS3UserShow) Help() string { return `show details of an S3 IAM user s3.user.show -name + + Output: JSON object with user details. ` } @@ -35,6 +37,28 @@ func (c *commandS3UserShow) HasTag(CommandTag) bool { return false } +type s3CredentialInfo struct { + AccessKey string `json:"access_key"` + Status string `json:"status"` +} + +type s3AccountInfo struct { + ID string `json:"id,omitempty"` + DisplayName string `json:"display_name,omitempty"` + Email string `json:"email,omitempty"` +} + +type s3UserShowResult struct { + Name string `json:"name"` + Status string `json:"status"` + Source string `json:"source"` + Account *s3AccountInfo `json:"account,omitempty"` + Policies []string `json:"policies"` + Actions []string `json:"actions,omitempty"` + Credentials []s3CredentialInfo `json:"credentials"` + ServiceAccounts []string `json:"service_accounts,omitempty"` +} + func (c *commandS3UserShow) Do(args []string, commandEnv *CommandEnv, writer io.Writer) error { f := flag.NewFlagSet(c.Name(), flag.ContinueOnError) name := f.String("name", "", "user name") @@ -69,51 +93,45 @@ func (c *commandS3UserShow) Do(args []string, commandEnv *CommandEnv, writer io. source = "static" } - fmt.Fprintf(writer, "Name: %s\n", id.Name) - fmt.Fprintf(writer, "Status: %s\n", status) - fmt.Fprintf(writer, "Source: %s\n", source) + result := s3UserShowResult{ + Name: id.Name, + Status: status, + Source: source, + } - if id.Account != nil { - if id.Account.Id != "" { - fmt.Fprintf(writer, "Account: %s", id.Account.Id) - if id.Account.DisplayName != "" { - fmt.Fprintf(writer, " (%s)", id.Account.DisplayName) - } - fmt.Fprintln(writer) - } - if id.Account.EmailAddress != "" { - fmt.Fprintf(writer, "Email: %s\n", id.Account.EmailAddress) + if id.Account != nil && (id.Account.Id != "" || id.Account.DisplayName != "" || id.Account.EmailAddress != "") { + result.Account = &s3AccountInfo{ + ID: id.Account.Id, + DisplayName: id.Account.DisplayName, + Email: id.Account.EmailAddress, } } - if len(id.PolicyNames) > 0 { - fmt.Fprintf(writer, "Policies: %s\n", strings.Join(id.PolicyNames, ", ")) - } else { - fmt.Fprintln(writer, "Policies: (none)") + result.Policies = id.PolicyNames + if result.Policies == nil { + result.Policies = []string{} } if len(id.Actions) > 0 { - fmt.Fprintf(writer, "Actions: %s\n", strings.Join(id.Actions, ", ")) + result.Actions = id.Actions } - fmt.Fprintln(writer) - if len(id.Credentials) > 0 { - fmt.Fprintln(writer, "Credentials:") - for _, cred := range id.Credentials { - st := cred.Status - if st == "" { - st = "Active" - } - fmt.Fprintf(writer, " %s %s\n", cred.AccessKey, st) + result.Credentials = make([]s3CredentialInfo, 0, len(id.Credentials)) + for _, cred := range id.Credentials { + st := cred.Status + if st == "" { + st = "Active" } - } else { - fmt.Fprintln(writer, "Credentials: (none)") + result.Credentials = append(result.Credentials, s3CredentialInfo{ + AccessKey: cred.AccessKey, + Status: st, + }) } if len(id.ServiceAccountIds) > 0 { - fmt.Fprintf(writer, "\nService Accounts: %s\n", strings.Join(id.ServiceAccountIds, ", ")) + result.ServiceAccounts = id.ServiceAccountIds } - return nil + return json.NewEncoder(writer).Encode(result) }, commandEnv.option.FilerAddress.ToGrpcAddress(), false, commandEnv.option.GrpcDialOption) } diff --git a/weed/shell/shell_liner.go b/weed/shell/shell_liner.go index 2d756dc63..cc17e80f2 100644 --- a/weed/shell/shell_liner.go +++ b/weed/shell/shell_liner.go @@ -1,6 +1,7 @@ package shell import ( + "bufio" "context" "fmt" "io" @@ -64,31 +65,46 @@ func RunShell(options ShellOptions) { } return nil }) - fmt.Printf("master: %s ", *options.Masters) + fmt.Fprintf(os.Stderr, "master: %s ", *options.Masters) if len(filers) > 0 { - fmt.Printf("filers: %v", filers) + fmt.Fprintf(os.Stderr, "filers: %v", filers) commandEnv.option.FilerAddress = filers[rand.IntN(len(filers))] } - fmt.Println() + fmt.Fprintln(os.Stderr) } - for { - cmd, err := line.Prompt("> ") - if err != nil { - if err != io.EOF { - fmt.Printf("%v\n", err) - } - return - } - - if strings.TrimSpace(cmd) != "" { - line.AppendHistory(cmd) - } - - for _, c := range util.StringSplit(cmd, ";") { - if processEachCmd(c, commandEnv) { + if liner.TerminalSupported() { + for { + cmd, err := line.Prompt("> ") + if err != nil { + if err != io.EOF { + fmt.Fprintf(os.Stderr, "%v\n", err) + } return } + + if strings.TrimSpace(cmd) != "" { + line.AppendHistory(cmd) + } + + for _, c := range util.StringSplit(cmd, ";") { + if processEachCmd(c, commandEnv) { + return + } + } + } + } else { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + cmd := scanner.Text() + for _, c := range util.StringSplit(cmd, ";") { + if processEachCmd(c, commandEnv) { + return + } + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err) } } } @@ -232,10 +248,10 @@ func loadHistory() { func saveHistory() { if f, err := os.Create(historyPath); err != nil { - fmt.Printf("Error creating history file: %v\n", err) + fmt.Fprintf(os.Stderr, "Error creating history file: %v\n", err) } else { if _, err = line.WriteHistory(f); err != nil { - fmt.Printf("Error writing history file: %v\n", err) + fmt.Fprintf(os.Stderr, "Error writing history file: %v\n", err) } f.Close() }