diff --git a/cmd/pinniped/cmd/exchange_credential.go b/cmd/pinniped/cmd/exchange_credential.go index a9f3566f..87f7b0f2 100644 --- a/cmd/pinniped/cmd/exchange_credential.go +++ b/cmd/pinniped/cmd/exchange_credential.go @@ -23,19 +23,39 @@ import ( //nolint: gochecknoinits func init() { - exchangeCredentialCmd := &cobra.Command{ - Run: runExchangeCredential, + rootCmd.AddCommand(newExchangeCredentialCmd(os.Args, os.Stdout, os.Stderr).cmd) +} + +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 Use: "exchange-credential", Short: "Exchange a credential for a cluster-specific access credential", Long: 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 @@ -43,13 +63,17 @@ func init() { 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 `), } - rootCmd.AddCommand(exchangeCredentialCmd) + c.cmd.SetArgs(args) + c.cmd.SetOut(stdout) + c.cmd.SetErr(stderr) + + return c } 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") -func runExchangeCredential(_ *cobra.Command, _ []string) { - err := exchangeCredential(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second) +func runExchangeCredential(stdout, _ io.Writer) { + err := exchangeCredential(os.LookupEnv, client.ExchangeToken, stdout, 30*time.Second) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) diff --git a/cmd/pinniped/cmd/exchange_credential_test.go b/cmd/pinniped/cmd/exchange_credential_test.go index db834020..0bf77bdc 100644 --- a/cmd/pinniped/cmd/exchange_credential_test.go +++ b/cmd/pinniped/cmd/exchange_credential_test.go @@ -9,6 +9,7 @@ import ( "bytes" "context" "fmt" + "io" "testing" "time" @@ -18,9 +19,105 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "github.com/suzerain-io/pinniped/internal/here" "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) { spec.Run(t, "cmd.exchangeCredential", func(t *testing.T, when spec.G, it spec.S) { var r *require.Assertions diff --git a/cmd/pinniped/cmd/get_kubeconfig_test.go b/cmd/pinniped/cmd/get_kubeconfig_test.go index 87ad7658..06738998 100644 --- a/cmd/pinniped/cmd/get_kubeconfig_test.go +++ b/cmd/pinniped/cmd/get_kubeconfig_test.go @@ -26,46 +26,47 @@ import ( "github.com/suzerain-io/pinniped/internal/here" ) -const ( - knownGoodUsage = ` -Usage: - get-kubeconfig [flags] +var ( + knownGoodUsageForGetKubeConfig = here.Doc(` + Usage: + get-kubeconfig [flags] -Flags: - -h, --help help for get-kubeconfig - --kubeconfig string Path to the kubeconfig file - --kubeconfig-context string Kubeconfig context override - --pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped") - --token string Credential to include in the resulting kubeconfig output (Required) + Flags: + -h, --help help for get-kubeconfig + --kubeconfig string Path to the kubeconfig file + --kubeconfig-context string Kubeconfig context override + --pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped") + --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 -kubeconfig context in order to access Pinniped's metadata. -The current kubeconfig is found similar to how kubectl finds it: -using the value of the --kubeconfig option, or if that is not -specified then from the value of the KUBECONFIG environment -variable, or if that is not specified then it defaults to -.kube/config in your home directory. + Requires admin-like access to the cluster using the current + kubeconfig context in order to access Pinniped's metadata. + The current kubeconfig is found similar to how kubectl finds it: + using the value of the --kubeconfig option, or if that is not + specified then from the value of the KUBECONFIG environment + variable, or if that is not specified then it defaults to + .kube/config in your home directory. -Prints a kubeconfig which is suitable to access the cluster using -Pinniped as the authentication mechanism. This kubeconfig output -can be saved to a file and used with future kubectl commands, e.g.: - pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig - kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods + Prints a kubeconfig which is suitable to access the cluster using + Pinniped as the authentication mechanism. This kubeconfig output + can be saved to a file and used with future kubectl commands, e.g.: + pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig + kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods -Usage: - get-kubeconfig [flags] + Usage: + get-kubeconfig [flags] -Flags: - -h, --help help for get-kubeconfig - --kubeconfig string Path to the kubeconfig file - --kubeconfig-context string Kubeconfig context override - --pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped") - --token string Credential to include in the resulting kubeconfig output (Required) -` + Flags: + -h, --help help for get-kubeconfig + --kubeconfig string Path to the kubeconfig file + --kubeconfig-context string Kubeconfig context override + --pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped") + --token string Credential to include in the resulting kubeconfig output (Required) + `) ) func TestNewGetKubeConfigCmd(t *testing.T) { @@ -126,7 +127,7 @@ func TestNewGetKubeConfigCmd(t *testing.T) { r.EqualError(c.cmd.Execute(), errorMessage) r.False(runFuncCalled) - output := "Error: " + errorMessage + knownGoodUsage + output := "Error: " + errorMessage + "\n" + knownGoodUsageForGetKubeConfig r.Equal(output, stdout.String()) r.Empty(stderr.String()) }) @@ -174,7 +175,7 @@ func TestNewGetKubeConfigCmd(t *testing.T) { r.EqualError(c.cmd.Execute(), errorMessage) r.False(runFuncCalled) - output := "Error: " + errorMessage + knownGoodUsage + output := "Error: " + errorMessage + "\n" + knownGoodUsageForGetKubeConfig r.Equal(output, stdout.String()) r.Empty(stderr.String()) }) @@ -195,7 +196,7 @@ func TestNewGetKubeConfigCmd(t *testing.T) { r.NoError(c.cmd.Execute()) r.False(runFuncCalled) - r.Equal(knownGoodHelp, stdout.String()) + r.Equal(knownGoodHelpForGetKubeConfig, stdout.String()) r.Empty(stderr.String()) }) }, spec.Parallel(), spec.Report(report.Terminal{}))