Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit b7f3f23

Browse files
Merge pull request #274 from secrethub/feature/refactor-ui
Refactor ui package
2 parents 0f49f1c + 21ad77e commit b7f3f23

8 files changed

Lines changed: 83 additions & 76 deletions

File tree

internals/cli/ui/ask.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@ func AskSecret(io IO, question string) (string, error) {
6262
return "", err
6363
}
6464

65-
raw, err := promptIn.ReadPassword()
65+
secret, err := readPassword(promptIn)
6666
if err != nil {
6767
return "", ErrReadInput(err)
6868
}
6969

7070
fmt.Fprintln(promptOut, "")
7171

72-
return string(raw), nil
72+
return secret, nil
7373
}
7474

7575
// AskMultiline prints out the question and reads back the input until an EOF is reached.

internals/cli/ui/io.go

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,37 @@ var (
1818

1919
// IO is an interface to work with input/output.
2020
type IO interface {
21-
Stdin() Reader
22-
Stdout() Writer
23-
Prompts() (Reader, Writer, error)
21+
Stdin() io.Reader
22+
Stdout() io.Writer
23+
Prompts() (io.Reader, io.Writer, error)
24+
IsStdinPiped() bool
25+
IsStdoutPiped() bool
2426
}
2527

2628
// UserIO is a middleware between input and output to the CLI program.
2729
// It implements userIO.Prompter and can be passed to libraries.
2830
type UserIO struct {
29-
Input Reader
30-
Output Writer
31-
tty file
31+
Input *os.File
32+
Output *os.File
33+
tty *os.File
3234
ttyAvailable bool
3335
}
3436

3537
// NewStdUserIO creates a new UserIO middleware only from os.Stdin and os.Stdout.
3638
func NewStdUserIO() UserIO {
3739
return UserIO{
38-
Input: file{os.Stdin},
39-
Output: file{os.Stdout},
40+
Input: os.Stdin,
41+
Output: os.Stdout,
4042
}
4143
}
4244

4345
// Stdin returns the UserIO's Input.
44-
func (o UserIO) Stdin() Reader {
46+
func (o UserIO) Stdin() io.Reader {
4547
return o.Input
4648
}
4749

4850
// Stdout returns the UserIO's Output.
49-
func (o UserIO) Stdout() Writer {
51+
func (o UserIO) Stdout() io.Writer {
5052
return o.Output
5153
}
5254

@@ -55,8 +57,8 @@ func (o UserIO) Stdout() Writer {
5557
// bypass stdin and stdout by connecting to /dev/tty on Unix systems when
5658
// available. On systems where tty is not available and when either input
5759
// or output is piped, prompting is not possible so an error is returned.
58-
func (o UserIO) Prompts() (Reader, Writer, error) {
59-
if o.Input.IsPiped() || o.Output.IsPiped() {
60+
func (o UserIO) Prompts() (io.Reader, io.Writer, error) {
61+
if o.IsStdoutPiped() || o.IsStdinPiped() {
6062
if o.ttyAvailable {
6163
return o.tty, o.tty, nil
6264
}
@@ -65,12 +67,30 @@ func (o UserIO) Prompts() (Reader, Writer, error) {
6567
return o.Input, o.Output, nil
6668
}
6769

68-
// Reader can read input for a CLI program.
69-
type Reader interface {
70-
io.Reader
71-
// ReadPassword reads a line of input from a terminal without local echo.
72-
ReadPassword() ([]byte, error)
73-
IsPiped() bool
70+
func (o UserIO) IsStdinPiped() bool {
71+
return isPiped(o.Input)
72+
}
73+
74+
func (o UserIO) IsStdoutPiped() bool {
75+
return isPiped(o.Output)
76+
}
77+
78+
// readPassword reads one line of input from the terminal without echoing the user input.
79+
func readPassword(r io.Reader) (string, error) {
80+
file, ok := r.(*os.File)
81+
if !ok {
82+
return "", ErrCannotAsk
83+
}
84+
// this case happens among other things when input is piped and ReadPassword is called.
85+
if !terminal.IsTerminal(int(file.Fd())) {
86+
return "", ErrCannotAsk
87+
}
88+
89+
password, err := terminal.ReadPassword(int(file.Fd()))
90+
if err != nil {
91+
return "", err
92+
}
93+
return string(password), nil
7494
}
7595

7696
// Readln reads 1 line of input from a io.Reader. The newline character is not included in the response.
@@ -84,31 +104,10 @@ func Readln(r io.Reader) (string, error) {
84104
return s.Text(), nil
85105
}
86106

87-
// Writer can write output for a CLI program.
88-
type Writer interface {
89-
io.Writer
90-
IsPiped() bool
91-
}
92-
93-
// file implements the Reader and Writer interface.
94-
type file struct {
95-
*os.File
96-
}
97-
98-
// ReadPassword reads from a terminal without echoing back the typed input.
99-
func (f file) ReadPassword() ([]byte, error) {
100-
// this case happens among other things when input is piped and ReadPassword is called.
101-
if !terminal.IsTerminal(int(f.Fd())) {
102-
return nil, ErrCannotAsk
103-
}
104-
105-
return terminal.ReadPassword(int(f.Fd()))
106-
}
107-
108-
// IsPiped checks whether the file is a pipe.
107+
// isPiped checks whether the file is a pipe.
109108
// If the file does not exist, it returns false.
110-
func (f file) IsPiped() bool {
111-
stat, err := f.Stat()
109+
func isPiped(file *os.File) bool {
110+
stat, err := file.Stat()
112111
if err != nil {
113112
return false
114113
}

internals/cli/ui/io_unix.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ func NewUserIO() UserIO {
99
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
1010
if err == nil {
1111
return UserIO{
12-
Input: file{os.Stdin},
13-
Output: file{os.Stdout},
14-
tty: file{tty},
12+
Input: os.Stdin,
13+
Output: os.Stdout,
14+
tty: tty,
1515
ttyAvailable: true,
1616
}
1717
}

internals/cli/ui/testing.go

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package ui
55
import (
66
"bytes"
77
"errors"
8+
"io"
89
)
910

1011
// FakeIO is a helper type for testing that implements the ui.IO interface
@@ -35,20 +36,28 @@ func NewFakeIO() *FakeIO {
3536
}
3637

3738
// Stdin returns the mocked StdIn.
38-
func (f *FakeIO) Stdin() Reader {
39+
func (f *FakeIO) Stdin() io.Reader {
3940
return f.StdIn
4041
}
4142

4243
// Stdout returns the mocked StdOut.
43-
func (f *FakeIO) Stdout() Writer {
44+
func (f *FakeIO) Stdout() io.Writer {
4445
return f.StdOut
4546
}
4647

4748
// Prompts returns the mocked prompts and error.
48-
func (f *FakeIO) Prompts() (Reader, Writer, error) {
49+
func (f *FakeIO) Prompts() (io.Reader, io.Writer, error) {
4950
return f.PromptIn, f.PromptOut, f.PromptErr
5051
}
5152

53+
func (f *FakeIO) IsStdinPiped() bool {
54+
return f.StdIn.Piped
55+
}
56+
57+
func (f *FakeIO) IsStdoutPiped() bool {
58+
return f.StdOut.Piped
59+
}
60+
5261
// FakeReader implements the Reader interface.
5362
type FakeReader struct {
5463
*bytes.Buffer
@@ -58,20 +67,6 @@ type FakeReader struct {
5867
ReadErr error
5968
}
6069

61-
// ReadPassword reads a line from the mocked buffer.
62-
func (f *FakeReader) ReadPassword() ([]byte, error) {
63-
pass, err := Readln(f)
64-
if err != nil {
65-
return nil, err
66-
}
67-
return []byte(pass), nil
68-
}
69-
70-
// IsPiped returns the mocked Piped.
71-
func (f *FakeReader) IsPiped() bool {
72-
return f.Piped
73-
}
74-
7570
// Read returns the mocked ReadErr or reads from the mocked buffer.
7671
func (f *FakeReader) Read(p []byte) (n int, err error) {
7772
if f.ReadErr != nil {
@@ -92,8 +87,3 @@ type FakeWriter struct {
9287
*bytes.Buffer
9388
Piped bool
9489
}
95-
96-
// IsPiped returns the mocked Piped.
97-
func (f *FakeWriter) IsPiped() bool {
98-
return f.Piped
99-
}

internals/secrethub/inject.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (cmd *InjectCommand) Run() error {
9191
return ErrReadFile(cmd.inFile, err)
9292
}
9393
} else {
94-
if !cmd.io.Stdin().IsPiped() {
94+
if !cmd.io.IsStdinPiped() {
9595
return ErrNoDataOnStdin
9696
}
9797

@@ -139,7 +139,7 @@ func (cmd *InjectCommand) Run() error {
139139
} else if cmd.outFile != "" {
140140
_, err := os.Stat(cmd.outFile)
141141
if err == nil && !cmd.force {
142-
if cmd.io.Stdout().IsPiped() {
142+
if cmd.io.IsStdoutPiped() {
143143
return ErrFileAlreadyExists
144144
}
145145

internals/secrethub/service_deploy_winrm.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func (cmd *ServiceDeployWinRmCommand) Run() error {
179179

180180
deployer := newWindowsDeployer(client, destinationPath)
181181

182-
if !cmd.io.Stdin().IsPiped() {
182+
if !cmd.io.IsStdinPiped() {
183183
return ErrNoDataOnStdin
184184
}
185185

internals/secrethub/write.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var (
2222
// WriteCommand is a command to write content to a secret.
2323
type WriteCommand struct {
2424
io ui.IO
25+
askSecret func(io ui.IO, question string) (string, error)
2526
path api.SecretPath
2627
inFile string
2728
multiline bool
@@ -36,6 +37,7 @@ func NewWriteCommand(io ui.IO, newClient newClientFunc) *WriteCommand {
3637
return &WriteCommand{
3738
clipper: clip.NewClipboard(),
3839
io: io,
40+
askSecret: ui.AskSecret,
3941
newClient: newClient,
4042
}
4143
}
@@ -82,7 +84,7 @@ func (cmd *WriteCommand) Run() error {
8284
if err != nil {
8385
return ErrReadFile(cmd.inFile, err)
8486
}
85-
} else if cmd.io.Stdin().IsPiped() {
87+
} else if cmd.io.IsStdinPiped() {
8688
data, err = ioutil.ReadAll(cmd.io.Stdin())
8789
if err != nil {
8890
return ui.ErrReadInput(err)
@@ -94,7 +96,7 @@ func (cmd *WriteCommand) Run() error {
9496
return err
9597
}
9698
} else {
97-
str, err := ui.AskSecret(cmd.io, "Please type in the value of the secret, followed by an [ENTER]:")
99+
str, err := cmd.askSecret(cmd.io, "Please type in the value of the secret, followed by an [ENTER]:")
98100
if err != nil {
99101
return err
100102
}

internals/secrethub/write_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package secrethub
22

33
import (
4+
"bufio"
45
"bytes"
56
"testing"
67

@@ -18,6 +19,19 @@ import (
1819
func TestWriteCommand_Run(t *testing.T) {
1920
testErr := errio.Namespace("test").Code("test").Error("test error")
2021

22+
fakeAskSecretFunc := func(io ui.IO, question string) (s string, err error) {
23+
reader, writer, err := io.Prompts()
24+
if err != nil {
25+
return "", err
26+
}
27+
_, err = writer.Write([]byte(question + "\n"))
28+
if err != nil {
29+
return "", err
30+
}
31+
line, _, err := bufio.NewReader(reader).ReadLine()
32+
return string(line), err
33+
}
34+
2135
cases := map[string]struct {
2236
cmd WriteCommand
2337
writeFunc func(path string, data []byte) (*api.SecretVersion, error)
@@ -121,7 +135,8 @@ func TestWriteCommand_Run(t *testing.T) {
121135
},
122136
"ask secret success": {
123137
cmd: WriteCommand{
124-
path: "namespace/repo/secret",
138+
path: "namespace/repo/secret",
139+
askSecret: fakeAskSecretFunc,
125140
},
126141
promptIn: "asked secret value",
127142
promptOut: "Please type in the value of the secret, followed by an [ENTER]:\n",
@@ -137,7 +152,8 @@ func TestWriteCommand_Run(t *testing.T) {
137152
},
138153
"ask secret error": {
139154
cmd: WriteCommand{
140-
path: "namespace/repo/secret",
155+
path: "namespace/repo/secret",
156+
askSecret: fakeAskSecretFunc,
141157
},
142158
promptErr: testErr,
143159
err: testErr,

0 commit comments

Comments
 (0)