Skip to content

Commit

Permalink
Use io.MultiWriter
Browse files Browse the repository at this point in the history
  • Loading branch information
anujc25 committed Aug 17, 2023
1 parent 79f6bfc commit f187b51
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 32 deletions.
84 changes: 55 additions & 29 deletions plugin/sync_plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package plugin
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
Expand All @@ -23,39 +24,60 @@ const (

// cmdOptions specifies the command options
type cmdOptions struct {
Stdout io.Writer
Stderr io.Writer
outWriter io.Writer
errWriter io.Writer
}

type CommandOptions func(o *cmdOptions)

// WithStdout specifies the CommandOption for configuring Stdout
func WithStdout(stdout io.Writer) CommandOptions {
// WithOutputWriter specifies the CommandOption for configuring Stdout
func WithOutputWriter(outWriter io.Writer) CommandOptions {
return func(o *cmdOptions) {
o.Stdout = stdout
o.outWriter = outWriter
}
}

// WithStdout specifies the CommandOption for configuring Stderr
func WithStderr(stderr io.Writer) CommandOptions {
// WithErrorWriter specifies the CommandOption for configuring Stderr
func WithErrorWriter(errWriter io.Writer) CommandOptions {
return func(o *cmdOptions) {
o.Stderr = stderr
o.errWriter = errWriter
}
}

func runCommand(commandPath string, args []string, opts *cmdOptions) error {
// WithNoStdout specifies to ignore stdout
func WithNoStdout() CommandOptions {
return func(o *cmdOptions) {
o.outWriter = io.Discard
}
}

// WithNoStderr specifies to ignore stderr
func WithNoStderr() CommandOptions {
return func(o *cmdOptions) {
o.errWriter = io.Discard
}
}

func runCommand(commandPath string, args []string, opts *cmdOptions) (bytes.Buffer, bytes.Buffer, error) {
command := exec.Command(commandPath, args...)
if opts.Stdout != nil {
command.Stdout = opts.Stdout
} else {
command.Stdout = os.Stdout

var stderr bytes.Buffer
var stdout bytes.Buffer

wout := io.MultiWriter(&stdout, os.Stdout)
werr := io.MultiWriter(&stderr, os.Stderr)

if opts.outWriter != nil {
wout = io.MultiWriter(&stdout, opts.outWriter)
}
if opts.Stderr != nil {
command.Stderr = opts.Stderr
} else {
command.Stderr = os.Stderr
if opts.errWriter != nil {
werr = io.MultiWriter(&stderr, opts.errWriter)
}
return command.Run()

command.Stdout = wout
command.Stderr = werr

return stdout, stderr, command.Run()
}

// SyncPluginsForTarget will attempt to install plugins required by the active
Expand All @@ -68,11 +90,16 @@ func runCommand(commandPath string, args []string, opts *cmdOptions) error {
// implementation are subjected to change/removal if an alternative means to
// provide equivalent functionality can be introduced.
//
// To write the logs to different stdout and stderr as part of the plugin sync
// command invocation configure CommandOptions as part of the parameters.
// To write the logs to different output and error streams as part of the plugin sync
// command invocation, configure CommandOptions as part of the parameters.
// By default this api will write to stdout and stderr.
// Example: SyncPluginsForTarget(types.TargetK8s, WithStdout(os.Stdout), WithStderr(os.Stderr))
func SyncPluginsForTarget(target types.Target, opts ...CommandOptions) error {
//
// Example:
//
// var outBuf bytes.Buffer
// var errBuf bytes.Buffer
// SyncPluginsForTarget(types.TargetK8s, WithOutputWriter(outBuf), WithErrorWriter(errBuf))
func SyncPluginsForTarget(target types.Target, opts ...CommandOptions) (string, error) {
// For now, the implementation expects env var TANZU_BIN to be set and
// pointing to the core CLI binary used to invoke the plugin sync with.

Expand All @@ -83,7 +110,7 @@ func SyncPluginsForTarget(target types.Target, opts ...CommandOptions) error {

cliPath := os.Getenv("TANZU_BIN")
if cliPath == "" {
return errors.New("the environment variable TANZU_BIN is not set")
return "", errors.New("the environment variable TANZU_BIN is not set")
}

altCommandArgs := []string{customCommandName}
Expand All @@ -94,13 +121,12 @@ func SyncPluginsForTarget(target types.Target, opts ...CommandOptions) error {

// Check if there is an alternate means to perform the plugin syncing
// operation, if not fall back to `plugin sync`
var stdout bytes.Buffer
var stderr bytes.Buffer
err := runCommand(cliPath, altCommandArgs, &cmdOptions{Stdout: &stdout, Stderr: &stderr})
if err == nil && stdout.String() != "" {
args = strings.Fields(stdout.String())
stdoutOutput, _, err := runCommand(cliPath, altCommandArgs, &cmdOptions{outWriter: io.Discard, errWriter: io.Discard})
if err == nil && stdoutOutput.String() != "" {
args = strings.Fields(stdoutOutput.String())
}

// Runs the actual command
return runCommand(cliPath, args, options)
stdoutOutput, stderrOutput, err := runCommand(cliPath, args, options)
return fmt.Sprintf("%s%s", stdoutOutput.String(), stderrOutput.String()), err
}
48 changes: 45 additions & 3 deletions plugin/sync_plugins_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,62 @@ func TestSyncPlugins(t *testing.T) {
t.Run(spec.test, func(t *testing.T) {
assert := assert.New(t)

// Set up stdout and stderr for our test
r, w, err := os.Pipe()
if err != nil {
t.Error(err)
}
c := make(chan []byte)
go readOutput(t, r, c)
stdout := os.Stdout
stderr := os.Stderr
defer func() {
os.Stdout = stdout
os.Stderr = stderr
}()
os.Stdout = w
os.Stderr = w

cliPath, err := setupFakeCLI(dir, spec.exitStatus, spec.newCommandExitStatus, spec.enableCustomCommand)
assert.Nil(err)
os.Setenv("TANZU_BIN", cliPath)

var output bytes.Buffer
// Test-1:
// - verify correct combinedOutput string returned as part of the output
// - verify correct string gets printed to default stdout and stderr
combinedOutput, err := SyncPluginsForTarget(types.TargetK8s)
w.Close()
stdoutRecieved := <-c

err = SyncPluginsForTarget(types.TargetK8s, WithStdout(&output), WithStderr(&output))
if spec.expectedFailure {
assert.NotNil(err)
} else {
assert.Nil(err)
}
assert.Equal(spec.expectedOutput, combinedOutput, "incorrect combinedOutput result")
assert.Equal(spec.expectedOutput, string(stdoutRecieved), "incorrect combinedOutput result")

// Test-2: when external stdout and stderr are provided with WithStdout, WithStderr options,
// verify correct string gets printed to provided custom stdout/stderr
var combinedOutputBuff bytes.Buffer
combinedOutput, err = SyncPluginsForTarget(types.TargetK8s, WithOutputWriter(&combinedOutputBuff), WithErrorWriter(&combinedOutputBuff))
if spec.expectedFailure {
assert.NotNil(err)
} else {
assert.Nil(err)
}
assert.Equal(spec.expectedOutput, combinedOutput, "incorrect combinedOutput result when external stdout/stderr is provided")
assert.Equal(spec.expectedOutput, combinedOutputBuff.String(), "incorrect combinedOutputBuff result")

// Test-3: when user asks to discard the stdout and stderr, it should not print it to any stdout/stderr by default
// but still return the combinedOutput string as part of the function return value
combinedOutput, err = SyncPluginsForTarget(types.TargetK8s, WithNoStdout(), WithNoStderr())
if spec.expectedFailure {
assert.NotNil(err)
} else {
assert.Nil(err)
}
assert.Equal(spec.expectedOutput, output.String())
assert.Equal(spec.expectedOutput, combinedOutput, "incorrect combinedOutput result when external stdout/stderr is provided")

os.Unsetenv("TANZU_BIN")
})
Expand Down

0 comments on commit f187b51

Please sign in to comment.