Add help/usage units for CLI exchange-credential subcommand

This commit is contained in:
Ryan Richard 2020-09-15 09:05:40 -07:00
parent 831df90c93
commit 4ced58b5b7
3 changed files with 166 additions and 44 deletions

View File

@ -23,8 +23,28 @@ import (
//nolint: gochecknoinits //nolint: gochecknoinits
func init() { func init() {
exchangeCredentialCmd := &cobra.Command{ rootCmd.AddCommand(newExchangeCredentialCmd(os.Args, os.Stdout, os.Stderr).cmd)
Run: runExchangeCredential, }
type exchangeCredentialCommand struct {
// runFunc is called by the cobra.Command.Run hook. It is included here for
// testability.
runFunc func(stdout, stderr io.Writer)
// cmd is the cobra.Command for this CLI command. It is included here for
// testability.
cmd *cobra.Command
}
func newExchangeCredentialCmd(args []string, stdout, stderr io.Writer) *exchangeCredentialCommand {
c := &exchangeCredentialCommand{
runFunc: runExchangeCredential,
}
c.cmd = &cobra.Command{
Run: func(cmd *cobra.Command, _ []string) {
c.runFunc(stdout, stderr)
},
Args: cobra.NoArgs, // do not accept positional arguments for this command Args: cobra.NoArgs, // do not accept positional arguments for this command
Use: "exchange-credential", Use: "exchange-credential",
Short: "Exchange a credential for a cluster-specific access credential", Short: "Exchange a credential for a cluster-specific access credential",
@ -49,7 +69,11 @@ func init() {
`), `),
} }
rootCmd.AddCommand(exchangeCredentialCmd) c.cmd.SetArgs(args)
c.cmd.SetOut(stdout)
c.cmd.SetErr(stderr)
return c
} }
type envGetter func(string) (string, bool) type envGetter func(string) (string, bool)
@ -57,8 +81,8 @@ type tokenExchanger func(ctx context.Context, token, caBundle, apiEndpoint strin
const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set") const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
func runExchangeCredential(_ *cobra.Command, _ []string) { func runExchangeCredential(stdout, _ io.Writer) {
err := exchangeCredential(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second) err := exchangeCredential(os.LookupEnv, client.ExchangeToken, stdout, 30*time.Second)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error()) _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1) os.Exit(1)

View File

@ -9,6 +9,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"io"
"testing" "testing"
"time" "time"
@ -18,9 +19,105 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"github.com/suzerain-io/pinniped/internal/here"
"github.com/suzerain-io/pinniped/internal/testutil" "github.com/suzerain-io/pinniped/internal/testutil"
) )
var (
knownGoodUsageForExchangeCredential = here.Doc(`
Usage:
exchange-credential [flags]
Flags:
-h, --help help for exchange-credential
`)
knownGoodHelpForExchangeCredential = here.Doc(`
Exchange a credential which proves your identity for a time-limited,
cluster-specific access credential.
Designed to be conveniently used as an credential plugin for kubectl.
See the help message for 'pinniped get-kubeconfig' for more
information about setting up a kubeconfig file using Pinniped.
Requires all of the following environment variables, which are
typically set in the kubeconfig:
- PINNIPED_TOKEN: the token to send to Pinniped for exchange
- PINNIPED_CA_BUNDLE: the CA bundle to trust when calling
Pinniped's HTTPS endpoint
- PINNIPED_K8S_API_ENDPOINT: the URL for the Pinniped credential
exchange API
For more information about credential plugins in general, see
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
Usage:
exchange-credential [flags]
Flags:
-h, --help help for exchange-credential
`)
)
func TestNewCredentialExchangeCmd(t *testing.T) {
spec.Run(t, "newCredentialExchangeCmd", func(t *testing.T, when spec.G, it spec.S) {
var r *require.Assertions
var stdout, stderr *bytes.Buffer
it.Before(func() {
r = require.New(t)
stdout, stderr = bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
})
it("calls runFunc and does not print usage or help when correct arguments and flags are used", func() {
c := newExchangeCredentialCmd([]string{}, stdout, stderr)
runFuncCalled := false
c.runFunc = func(out, err io.Writer) {
runFuncCalled = true
}
r.NoError(c.cmd.Execute())
r.True(runFuncCalled)
r.Empty(stdout.String())
r.Empty(stderr.String())
})
it("fails when args are passed", func() {
c := newExchangeCredentialCmd([]string{"some-arg"}, stdout, stderr)
runFuncCalled := false
c.runFunc = func(out, err io.Writer) {
runFuncCalled = true
}
errorMessage := `unknown command "some-arg" for "exchange-credential"`
r.EqualError(c.cmd.Execute(), errorMessage)
r.False(runFuncCalled)
output := "Error: " + errorMessage + "\n" + knownGoodUsageForExchangeCredential
r.Equal(output, stdout.String())
r.Empty(stderr.String())
})
it("prints a nice help message", func() {
c := newExchangeCredentialCmd([]string{"--help"}, stdout, stderr)
runFuncCalled := false
c.runFunc = func(out, err io.Writer) {
runFuncCalled = true
}
r.NoError(c.cmd.Execute())
r.False(runFuncCalled)
r.Equal(knownGoodHelpForExchangeCredential, stdout.String())
r.Empty(stderr.String())
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}
func TestExchangeCredential(t *testing.T) { func TestExchangeCredential(t *testing.T) {
spec.Run(t, "cmd.exchangeCredential", func(t *testing.T, when spec.G, it spec.S) { spec.Run(t, "cmd.exchangeCredential", func(t *testing.T, when spec.G, it spec.S) {
var r *require.Assertions var r *require.Assertions

View File

@ -26,46 +26,47 @@ import (
"github.com/suzerain-io/pinniped/internal/here" "github.com/suzerain-io/pinniped/internal/here"
) )
const ( var (
knownGoodUsage = ` knownGoodUsageForGetKubeConfig = here.Doc(`
Usage: Usage:
get-kubeconfig [flags] get-kubeconfig [flags]
Flags: Flags:
-h, --help help for get-kubeconfig -h, --help help for get-kubeconfig
--kubeconfig string Path to the kubeconfig file --kubeconfig string Path to the kubeconfig file
--kubeconfig-context string Kubeconfig context override --kubeconfig-context string Kubeconfig context override
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped") --pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
--token string Credential to include in the resulting kubeconfig output (Required) --token string Credential to include in the resulting kubeconfig output (Required)
` `)
knownGoodHelp = `Print a kubeconfig for authenticating into a cluster via Pinniped. knownGoodHelpForGetKubeConfig = here.Doc(`
Print a kubeconfig for authenticating into a cluster via Pinniped.
Requires admin-like access to the cluster using the current Requires admin-like access to the cluster using the current
kubeconfig context in order to access Pinniped's metadata. kubeconfig context in order to access Pinniped's metadata.
The current kubeconfig is found similar to how kubectl finds it: The current kubeconfig is found similar to how kubectl finds it:
using the value of the --kubeconfig option, or if that is not using the value of the --kubeconfig option, or if that is not
specified then from the value of the KUBECONFIG environment specified then from the value of the KUBECONFIG environment
variable, or if that is not specified then it defaults to variable, or if that is not specified then it defaults to
.kube/config in your home directory. .kube/config in your home directory.
Prints a kubeconfig which is suitable to access the cluster using Prints a kubeconfig which is suitable to access the cluster using
Pinniped as the authentication mechanism. This kubeconfig output Pinniped as the authentication mechanism. This kubeconfig output
can be saved to a file and used with future kubectl commands, e.g.: can be saved to a file and used with future kubectl commands, e.g.:
pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig
kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods
Usage: Usage:
get-kubeconfig [flags] get-kubeconfig [flags]
Flags: Flags:
-h, --help help for get-kubeconfig -h, --help help for get-kubeconfig
--kubeconfig string Path to the kubeconfig file --kubeconfig string Path to the kubeconfig file
--kubeconfig-context string Kubeconfig context override --kubeconfig-context string Kubeconfig context override
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped") --pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
--token string Credential to include in the resulting kubeconfig output (Required) --token string Credential to include in the resulting kubeconfig output (Required)
` `)
) )
func TestNewGetKubeConfigCmd(t *testing.T) { func TestNewGetKubeConfigCmd(t *testing.T) {
@ -126,7 +127,7 @@ func TestNewGetKubeConfigCmd(t *testing.T) {
r.EqualError(c.cmd.Execute(), errorMessage) r.EqualError(c.cmd.Execute(), errorMessage)
r.False(runFuncCalled) r.False(runFuncCalled)
output := "Error: " + errorMessage + knownGoodUsage output := "Error: " + errorMessage + "\n" + knownGoodUsageForGetKubeConfig
r.Equal(output, stdout.String()) r.Equal(output, stdout.String())
r.Empty(stderr.String()) r.Empty(stderr.String())
}) })
@ -174,7 +175,7 @@ func TestNewGetKubeConfigCmd(t *testing.T) {
r.EqualError(c.cmd.Execute(), errorMessage) r.EqualError(c.cmd.Execute(), errorMessage)
r.False(runFuncCalled) r.False(runFuncCalled)
output := "Error: " + errorMessage + knownGoodUsage output := "Error: " + errorMessage + "\n" + knownGoodUsageForGetKubeConfig
r.Equal(output, stdout.String()) r.Equal(output, stdout.String())
r.Empty(stderr.String()) r.Empty(stderr.String())
}) })
@ -195,7 +196,7 @@ func TestNewGetKubeConfigCmd(t *testing.T) {
r.NoError(c.cmd.Execute()) r.NoError(c.cmd.Execute())
r.False(runFuncCalled) r.False(runFuncCalled)
r.Equal(knownGoodHelp, stdout.String()) r.Equal(knownGoodHelpForGetKubeConfig, stdout.String())
r.Empty(stderr.String()) r.Empty(stderr.String())
}) })
}, spec.Parallel(), spec.Report(report.Terminal{})) }, spec.Parallel(), spec.Report(report.Terminal{}))