Inside kubectl plugin, prompt for input? - go

I'm writing a kubectl plugin to authenticate users, and I would like to prompt the user for a password after the plugin is invoked. From what I understand, it's fairly trivial to get input from STDIN, but I'm struggling seeing messages written to STDOUT. Currently my code looks like this:
In cmd/kubectl-myauth.go:
// This is mostly boilerplate, but it's needed for the MRE
// https://stackoverflow.com/help/minimal-reproducible-example
package myauth
import (...)
func main() {
pflag.CommandLine = pflag.NewFlagSet("kubectl-myauth", pflag.ExitOnError)
root := cmd.NewCmdAuthOp(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
In pkg/cmd/auth.go:
package cmd
...
type AuthOpOptions struct {
configFlags *genericclioptions.ConfigFlags
resultingContext *api.Context
rawConfig api.Config
args []string
...
genericclioptions.IOStreams
}
func NewAuthOpOptions(streams genericclioptions.IOStreams) *AuthOpOptions {
return &AuthOpOptions{
configFlags: genericclioptions.NewConfigFlags(true),
IOStreams: streams,
}
}
func NewCmdAuthOp(streams genericclioptions.IOStreams) *cobra.Command {
o := NewAuthOpOptions(streams)
cmd := &cobra.Command{
RunE: func(c *cobra.Command, args []string) error {
return o.Run()
},
}
return cmd
}
func (o *AuthOpOptions) Run() error {
pass, err := getPassword(o)
if err != nil {
return err
}
// Do Auth Stuff
// Eventually print an ExecCredential to STDOUT
return nil
}
func getPassword(o *AuthOpOptions) (string, error) {
var reader *bufio.Reader
reader = nil
pass := ""
for pass == "" {
// THIS IS AN IMPORTANT LINE [1]
fmt.Fprintf(o.IOStreams.Out, "Password with which to authenticate:\n")
// THE REST OF THIS IS STILL IMPORTANT, BUT LESS SO [2]
if reader == nil {
// The first time through, initialize the reader
reader = bufio.NewReader(o.IOStreams.In)
}
pass, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
pass = strings.Trim(pass, "\r\n")
if pass == "" {
// ALSO THIS LINE IS IMPORTANT [3]
fmt.Fprintf(o.IOStreams.Out, `Read password was empty string.
Please input a valid password.
`)
}
}
return pass, nil
}
This works the way that I expect when running from outside of the kubectl context - namely, it prints the string, prompts for input, and continues. However, from inside the kubectl context, I believe the print between the first two all-caps comments ([1] and [2]) is being swallowed by kubectl listening on STDOUT. I can get around this by printing to STDERR, but that feels... wrong. Is there a way that I can bypass kubectl's consumption of STDOUT to communicate with the user?
TL;DR: kubectl appears to be swallowing all of STDOUT for kubectl plugins, but I want to prompt the user for input - is there a simple way to do this?

Sorry I have no better answer than "Works for me" :-) Here are the steps:
git clone https://github.com/kubernetes/kubernetes.git
duplicate sample-cli-plugin as test-cli-plugin (this involves fixing import-restrictions.yaml, rules-godeps.yaml and rules.yaml under staging/publishing - maybe not necessary, but it's safer this way)
change kubectl-ns.go to kubectl-test.go:
package main
import (
"os"
"github.com/spf13/pflag"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/test-cli-plugin/pkg/cmd"
)
func main() {
flags := pflag.NewFlagSet("kubectl-test", pflag.ExitOnError)
pflag.CommandLine = flags
root := cmd.NewCmdTest(genericclioptions.IOStreams{In: os.Stdin,
Out: os.Stdout,
ErrOut: os.Stderr})
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
change ns.go to test.go:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
)
type TestOptions struct {
configFlags *genericclioptions.ConfigFlags
genericclioptions.IOStreams
}
func NewTestOptions(streams genericclioptions.IOStreams) *TestOptions {
return &TestOptions{
configFlags: genericclioptions.NewConfigFlags(true),
IOStreams: streams,
}
}
func NewCmdTest(streams genericclioptions.IOStreams) *cobra.Command {
o := NewTestOptions(streams)
cmd := &cobra.Command{
Use: "test",
Short: "Test plugin",
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
o.Run()
return nil
},
}
return cmd
}
func (o *TestOptions) Run() error {
fmt.Fprintf(os.Stderr, "Testing Fprintf Stderr\n")
fmt.Fprintf(os.Stdout, "Testing Fprintf Stdout\n")
fmt.Printf("Testing Printf\n")
fmt.Fprintf(o.IOStreams.Out, "Testing Fprintf o.IOStreams.Out\n")
return nil
}
fix BUILD files accordingly
build the plugin
run make
copy kubectl-test to /usr/local/bin
run the compiled kubectl binary:
~/k8s/_output/bin$ ./kubectl test
Testing Fprintf Stderr
Testing Fprintf Stdout
Testing Printf
Testing Fprintf o.IOStreams.Out

Related

cannot convert (type string) to type errorString

I wrote a simple program which does executing uptime command and fetch its result. As part of error handling I am getting trouble while converting error type into string and string type to error. How can I achieve this ?
package main
import (
"fmt"
"os/exec"
)
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
func execCommand(cmd string) (string, error) {
if cmd == "" {
return "", &errorString("Passed empty input")
}
output, err := exec.Command(cmd).Output()
fmt.Println("Output: " ,string(output))
fmt.Println("Error: ", err)
if err != nil {
fmt.Printf("Received error %q while executing the command %q", err, cmd)
return "",err
}
fmt.Printf("Command executed successfully.\nOutput: %s\n",output)
return string(output), nil
}
func main() {
command := "uptime"
output, err := execCommand(command)
if err != nil {
fmt.Errorf("Received error while executing the command\n")
} else {
fmt.Printf("Command %s output %s ", command, output)
}
}
And while executing getting below error
agastya in uptime_cmd on  my-code-go
❯ go run execute_uptime_command_1.go
# command-line-arguments
./execute_uptime_command_1.go:18:28: cannot convert "Passed empty input" (type string) to type errorString
agastya in uptime_cmd on  my-code-go
What I am trying to achieve is, trying to convert a string into error and vice versa. I am trying to implement below test cases to above code
package main
import (
"testing"
"strings"
)
func TestExecCommand(t *testing.T) {
command := "uptime"
expectedIncludes := "load"
received, err := execCommand(command)
if !strings.Contains(received, expectedIncludes) {
t.Errorf("Expecting %q to include %q", received, expectedIncludes)
}
received, err = execCommand("")
if received != "" {
t.Errorf("Expecting empty response when command is empty")
}
received, err = execCommand("uptime1")
if !strings.Contains(string(err), "executable file not found in $PATH") {
t.Errorf("Expecting executable not found error while executing invalid command as 'uptime1'");
}
}
And Unable to proceed with above error. Any suggestions much appreicated. It dont have to be explicit solution, even reference articles are fine.
Thank you.
To create an error from string use
return "", &errorString{"Passed empty input"}

Validating flags using Cobra

The sketch below is a command line application written using Cobra and Go. I'd like to throw an error if the value of flag1 doesn't match the regex ^\s+\/\s+. How do I do that?
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)
var flag1 string
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "cobra-sketch",
Short: "Sketch for Cobra flags",
Long: "Sketch for Cobra flags",
Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Flag1 is %s\n", flag1)},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra-sketch.yaml)")
rootCmd.PersistentFlags().StringVar(&flag1, "flag1", "", "Value of Flag 1")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
cobra.CheckErr(err)
// Search config in home directory with name ".cobra-sketch" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".cobra-sketch")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
}
Let's say a user runs the command like this: cobra-sketch --flag1 "hello". "hello" will be stored in the var flag1 string variable you have assigned to the flag, to check if the input matches any regexp, you can do:
var rootCmd = &cobra.Command{
Use: "cobra-sketch",
...
RunE: func(cmd *cobra.Command, args []string) error {
// You can also use MustCompile if you are sure the regular expression
// is valid, it panics instead of returning an error
re, err := regexp.Compile(`^\s+\/\s+`)
if err != nil {
return err // Handle error
}
if !regexp.MatchString(flag1) {
return fmt.Errorf("invalid value: %q", flag1)
}
fmt.Printf("Flag1 is %s\n", flag1)
return nil
},
}

How can I invoke a default subcommand with cobra?

Using cobra, if my app is invoked without a specific action (but arguments), I'd like to run a default command:
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "mbmd",
Short: "ModBus Measurement Daemon",
Long: "Easily read and distribute data from ModBus meters and grid inverters",
Run: func(cmd *cobra.Command, args []string) {
run(cmd, args)
},
}
However, since the root command doesn't have all arguments the child command has this fails as it's apparently now aware of the child command's arguments:
❯ go run main.go -d sma:126#localhost:5061 --api 127.1:8081 -v
Error: unknown shorthand flag: 'd' in -d
as opposed to:
❯ go run main.go run -d sma:126#localhost:5061 --api 127.1:8081 -v
2019/07/29 20:58:10 mbmd unknown version (unknown commit)
How can I programmatically instantiate/invoke a child command?
Here is another solution:
cmd, _, err := rootCmd.Find(os.Args[1:])
// default cmd if no cmd is given
if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
args := append([]string{defaultCmd.Use}, os.Args[1:]...)
rootCmd.SetArgs(args)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
Replace defaultCmd with one you want to be default
This part cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp keeps help command working for root command if no arguments was set
March 2021: You might consider a workaround as the one presented in spf13/cobra issue 823
func subCommands() (commandNames []string) {
for _, command := range cmd.Commands() {
commandNames = append(commandNames, append(command.Aliases, command.Name())...)
}
return
}
func setDefaultCommandIfNonePresent() {
if len(os.Args) > 1 {
potentialCommand := os.Args[1]
for _, command := range subCommands() {
if command == potentialCommand {
return
}
}
os.Args = append([]string{os.Args[0], "<default subcommand>"}, os.Args[1:]...)
}
}
func main() {
setDefaultCommandIfNonePresent()
if err := cmd.Execute(); err != nil {
zap.S().Error(err)
os.Exit(1)
}
}
The difference here is that it checks if len(os.Args) > 1 before changing the default subcommand.
This means that, if ran without any arguments, it will print the default help command (with all of the subcommands).
Otherwise, if supplied any arguments, it will use the subcommand.
So, it will display the main 'help' without arguments, and the subcommand's help if supplied '-h'/'--help'.
Or (Oct. 2021), from the author of PR 823:
Latest solve for this is the following:
main.go
func main() {
// Define the default sub command 'defCmd' here. If user doesn't submit
// using a default command, we'll use what is here.
defCmd:="mydefaultcmd"
cmd.Execute(defCmd)
}
root.go
func Execute(defCmd string) {
var cmdFound bool
cmd :=rootCmd.Commands()
for _,a:=range cmd{
for _,b:=range os.Args[1:] {
if a.Name()==b {
cmdFound=true
break
}
}
}
if !cmdFound {
args:=append([]string{defCmd}, os.Args[1:]...)
rootCmd.SetArgs(args)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

Golang patterns for stdin testing

EDIT: Adrian's suggestion makes sense, so I moved my code into a function and called the function from my cobra block:
package cmd
import (
"fmt"
"log"
"os"
"io"
"github.com/spf13/cobra"
"github.com/spf13/viper"
input "github.com/tcnksm/go-input"
)
var configureCmd = &cobra.Command{
Use: "configure",
Short: "Configure your TFE credentials",
Long: `Prompts for your TFE API credentials, then writes them to
a configuration file (defaults to ~/.tgc.yaml`,
Run: func(cmd *cobra.Command, args []string) {
CreateConfigFileFromPrompts(os.Stdin, os.Stdout)
},
}
func CreateConfigFileFromPrompts(stdin io.Reader, stdout io.Writer) {
ui := &input.UI{
Writer: stdout,
Reader: stdin,
}
tfeURL, err := ui.Ask("TFE URL:", &input.Options{
Default: "https://app.terraform.io",
Required: true,
Loop: true,
})
if err != nil {
log.Fatal(err)
}
viper.Set("tfe_url", tfeURL)
tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
Default: "",
Required: true,
Loop: true,
Mask: true,
MaskDefault: true,
})
if err != nil {
log.Fatal(err)
}
viper.Set("tfe_api_token", tfeAPIToken)
configPath := ConfigPath()
viper.SetConfigFile(configPath)
err = viper.WriteConfig()
if err != nil {
log.Fatal("Failed to write to: ", configPath, " Error was: ", err)
}
fmt.Println("Saved to", configPath)
}
So what can I pass to this method to test that the output is as expected?
package cmd
import (
"strings"
"testing"
)
func TestCreateConfigFileFromPrompts(t *testing.T) {
// How do I pass the stdin and out to the method?
// Then how do I test their contents?
// CreateConfigFileFromPrompts()
}
func TestCreateConfigFileFromPrompts(t *testing.T) {
var in bytes.Buffer
var gotOut, wantOut bytes.Buffer
// The reader should read to the \n each of two times.
in.Write([]byte("example-url.com\nexampletoken\n"))
// wantOut could just be []byte, but for symmetry's sake I've used another buffer
wantOut.Write([]byte("TFE URL:TFE API Token (Create one at example-url.com/app/settings/tokens)"))
// I don't know enough about Viper to manage ConfigPath()
// but it seems youll have to do it here somehow.
configFilePath := "test/file/location"
CreateConfigFileFromPrompts(&in, &gotOut)
// verify that correct prompts were sent to the writer
if !bytes.Equal(gotOut.Bytes(), wantOut.Bytes()) {
t.Errorf("Prompts = %s, want %s", gotOut.Bytes(), wantOut.Bytes())
}
// May not need/want to test viper's writing of the config file here, or at all, but if so:
var fileGot, fileWant []byte
fileWant = []byte("Correct Config file contents:\n URL:example-url.com\nTOKEN:exampletoken")
fileGot, err := ioutil.ReadFile(configFilePath)
if err != nil {
t.Errorf("Error reading config file %s", configFilePath)
}
if !bytes.Equal(fileGot, fileWant) {
t.Errorf("ConfigFile: %s not created correctly got = %s, want %s", configFilePath, fileGot, fileWant)
}
}
As highlighted by #zdebra in comments to his answer, the go-input package is panicing and giving you the error: Reader must be a file. If you are married to using that package, you can avoid the problem by disabling the masking option on the ui.Ask for your second input:
tfeAPIToken, err := ui.Ask(fmt.Sprintf("TFE API Token (Create one at %s/app/settings/tokens)", tfeURL), &input.Options{
Default: "",
Required: true,
Loop: true,
//Mask: true, // if this is set to True, the input must be a file for some reason
//MaskDefault: true,
})
The reader and the writer need to be set up before the tested function is called. After is called, the result is written into the writer where it should be verified.
package cmd
import (
"strings"
"testing"
)
func TestCreateConfigFileFromPrompts(t *testing.T) {
in := strings.NewReader("<your input>") // you can use anything that satisfies io.Reader interface here
out := new(strings.Builder) // you could use anything that satisfies io.Writer interface here like bytes.Buffer
CreateConfigFileFromPrompts(in, out)
// here you verify the output written into the out
expectedOutput := "<your expected output>"
if out.String() != expectedOutput {
t.Errorf("expected %s to be equal to %s", out.String(), expectedOutput)
}
}

Mocking crypto/ssh/terminal

Has anyone had any success or have any ideas on what would be the best way to mock entry (for testing purposes) to a term.ReadPassword(int(os.Stdin.Fd())) call in the golang.org/x/crypto/ssh/terminal package?
I have tried creating a temp file (vs os.Stdin) and writing string values like testing\n or testing\r to the temp file but I get the error inappropriate ioctl for device. I'm guessing it is something TTY related or a specific format that is missing(?) but I really am not sure.
Help appreciated.
If you are stubbing this test by creating a fake file that os.Stdin is referencing, your tests will become tremendously OS specific when you try to handle ReadPassword(). This is because under the hood Go is compiling separate syscalls depending on the OS. ReadPassword() is implemented here, but the syscalls based on architecture and OS are in this directory. As you can see there are many. I cannot think of a good way to stub this test in the way you are specifying.
With the limited understanding of your problem the solution I would propose would be to inject a simple interface along the lines of:
type PasswordReader interface {
ReadPassword(fd int) ([]byte, error)
}
func (pr PasswordReader) ReadPassword(fd int) ([]byte, error) {
return terminal.ReadPassword(fd)
}
This way you can pass in a fake object to your tests, and stub the response to ReadPassword. I know this feels like writing your code for your tests, but you can reframe this thought as terminal is an outside dependency (I/O) that should be injected! So now your tests are not only ensuring your code works, but actually helping you make good design decisions.
Corbin's example prompted me to look into interface mocking and prompted me to write up a basic example:
// getter.go
package cmd
import (
"errors"
"syscall"
"golang.org/x/crypto/ssh/terminal"
)
type PasswordReader interface {
ReadPassword() (string, error)
}
type StdInPasswordReader struct {
}
func (pr StdInPasswordReader) ReadPassword() (string, error) {
pwd, error := terminal.ReadPassword(int(syscall.Stdin))
return string(pwd), error
}
func readPassword(pr PasswordReader) (string, error) {
pwd, err := pr.ReadPassword()
if err != nil {
return "", err
}
if len(pwd) == 0 {
return "", errors.New("empty password provided")
}
return pwd, nil
}
func Run(pr PasswordReader) (string, error) {
pwd, err := readPassword(pr)
if err != nil {
return "", err
}
return string(pwd), nil
}
In the test, we can mock errors and simulating no stdin input.
// getter_test.go
package cmd_test
import (
"errors"
"testing"
"github.com/petems/passwordgetter/cmd"
"github.com/stretchr/testify/assert"
)
type stubPasswordReader struct {
Password string
ReturnError bool
}
func (pr stubPasswordReader) ReadPassword() (string, error) {
if pr.ReturnError {
return "", errors.New("stubbed error")
}
return pr.Password, nil
}
func TestRunReturnsErrorWhenReadPasswordFails(t *testing.T) {
pr := stubPasswordReader{ReturnError: true}
result, err := cmd.Run(pr)
assert.Error(t, err)
assert.Equal(t, errors.New("stubbed error"), err)
assert.Equal(t, "", result)
}
func TestRunReturnsPasswordInput(t *testing.T) {
pr := stubPasswordReader{Password: "password"}
result, err := cmd.Run(pr)
assert.NoError(t, err)
assert.Equal(t, "password", result)
}
There are also tools like gomock, testify and counterfeiter that basically do all the heavy lifting for you, and you can add in generator steps into the code:
//go:generate mockgen -destination=../mocks/mock_getter.go -package=mocks github.com/petems/passwordgetter/cmd PasswordReader
You then include the mock that gets generated in your test:
package cmd_test
import (
"testing"
"errors"
"github.com/golang/mock/gomock"
"github.com/petems/passwordgetter/mocks"
"github.com/petems/passwordgetter/cmd"
"github.com/stretchr/testify/assert"
)
func TestRunReturnsErrorWhenEmptyString(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockPasswordReader := mocks.NewMockPasswordReader(mockCtrl)
mockPasswordReader.EXPECT().ReadPassword().Return("", nil).Times(1)
result, err := cmd.Run(mockPasswordReader)
assert.Error(t, err)
assert.Equal(t, errors.New("empty password provided"), err)
assert.Equal(t, "", result)
}

Resources