diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 82c4401a7..67f602d97 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -36,6 +36,7 @@ import ( ) type kubeconfigDeps struct { + getenv func(key string) string getPathToSelf func() (string, error) getClientset getConciergeClientsetFunc log plog.MinLogger @@ -43,6 +44,7 @@ type kubeconfigDeps struct { func kubeconfigRealDeps() kubeconfigDeps { return kubeconfigDeps{ + getenv: os.Getenv, getPathToSelf: os.Executable, getClientset: getRealConciergeClientset, log: plog.New(), @@ -156,7 +158,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { ), ) f.StringVar(&flags.oidc.upstreamIDPFlow, "upstream-identity-provider-flow", "", fmt.Sprintf("The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. '%s', '%s')", idpdiscoveryv1alpha1.IDPFlowCLIPassword, idpdiscoveryv1alpha1.IDPFlowBrowserAuthcode)) - f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file") + f.StringVar(&flags.kubeconfigPath, "kubeconfig", deps.getenv("KUBECONFIG"), "Path to kubeconfig file") f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)") f.BoolVar(&flags.skipValidate, "skip-validation", false, "Skip final validation of the kubeconfig (default: false)") f.DurationVar(&flags.timeout, "timeout", 10*time.Minute, "Timeout for autodiscovery and validation") diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 6f063fcd8..26a5e6077 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -97,6 +97,47 @@ func TestGetKubeconfig(t *testing.T) { }`, issuerURL) } + helpOutputFormatString := here.Doc(` + Generate a Pinniped-based kubeconfig for a cluster + + Usage: + kubeconfig [flags] + + Flags: + --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") + --concierge-authenticator-name string Concierge authenticator name (default: autodiscover) + --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) + --concierge-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge + --concierge-credential-issuer string Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover) + --concierge-endpoint string API base for the Concierge endpoint + --concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI) + --concierge-skip-wait Skip waiting for any pending Concierge strategies to become ready (default: false) + --credential-cache string Path to cluster-specific credentials cache + --generated-name-suffix string Suffix to append to generated cluster, context, user kubeconfig entries (default "-pinniped") + -h, --help help for kubeconfig + --install-hint string This text is shown to the user when the pinniped CLI is not installed. (default "The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli for more details") + --kubeconfig string Path to kubeconfig file%s + --kubeconfig-context string Kubeconfig context name (default: current active context) + --no-concierge Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly + --oidc-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) + --oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli") + --oidc-issuer string OpenID Connect issuer URL (default: autodiscover) + --oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only) + --oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange + --oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience,username,groups]) + --oidc-session-cache string Path to OpenID Connect session cache file + --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) + -o, --output string Output file path (default: stdout) + --pinniped-cli-path string Full path or executable name for the Pinniped CLI binary to be embedded in the resulting kubeconfig output (e.g. 'pinniped') (default: full path of the binary used to execute this command) + --skip-validation Skip final validation of the kubeconfig (default: false) + --static-token string Instead of doing an OIDC-based login, specify a static token + --static-token-env string Instead of doing an OIDC-based login, read a static token from the environment + --timeout duration Timeout for autodiscovery and validation (default 10m0s) + --upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'cli_password', 'browser_authcode') + --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') + `) + tests := []struct { name string args func(string, string) []string @@ -120,46 +161,17 @@ func TestGetKubeconfig(t *testing.T) { name: "help flag passed", args: func(issuerCABundle string, issuerURL string) []string { return []string{"--help"} }, wantStdout: func(issuerCABundle string, issuerURL string) string { - return here.Doc(` - Generate a Pinniped-based kubeconfig for a cluster - - Usage: - kubeconfig [flags] - - Flags: - --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") - --concierge-authenticator-name string Concierge authenticator name (default: autodiscover) - --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) - --concierge-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge - --concierge-credential-issuer string Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover) - --concierge-endpoint string API base for the Concierge endpoint - --concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI) - --concierge-skip-wait Skip waiting for any pending Concierge strategies to become ready (default: false) - --credential-cache string Path to cluster-specific credentials cache - --generated-name-suffix string Suffix to append to generated cluster, context, user kubeconfig entries (default "-pinniped") - -h, --help help for kubeconfig - --install-hint string This text is shown to the user when the pinniped CLI is not installed. (default "The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli for more details") - --kubeconfig string Path to kubeconfig file - --kubeconfig-context string Kubeconfig context name (default: current active context) - --no-concierge Generate a configuration which does not use the Concierge, but sends the credential to the cluster directly - --oidc-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) - --oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli") - --oidc-issuer string OpenID Connect issuer URL (default: autodiscover) - --oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only) - --oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange - --oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience,username,groups]) - --oidc-session-cache string Path to OpenID Connect session cache file - --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) - -o, --output string Output file path (default: stdout) - --pinniped-cli-path string Full path or executable name for the Pinniped CLI binary to be embedded in the resulting kubeconfig output (e.g. 'pinniped') (default: full path of the binary used to execute this command) - --skip-validation Skip final validation of the kubeconfig (default: false) - --static-token string Instead of doing an OIDC-based login, specify a static token - --static-token-env string Instead of doing an OIDC-based login, read a static token from the environment - --timeout duration Timeout for autodiscovery and validation (default 10m0s) - --upstream-identity-provider-flow string The type of client flow to use with the upstream identity provider during login with a Supervisor (e.g. 'cli_password', 'browser_authcode') - --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor - --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory', 'github') - `) + return fmt.Sprintf(helpOutputFormatString, "") + }, + }, + { + name: "help flag passed with KUBECONFIG env var set", + env: map[string]string{ + "KUBECONFIG": "/path/to/kubeconfig", + }, + args: func(issuerCABundle string, issuerURL string) []string { return []string{"--help"} }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return fmt.Sprintf(helpOutputFormatString, ` (default "/path/to/kubeconfig")`) }, }, { @@ -3237,6 +3249,9 @@ func TestGetKubeconfig(t *testing.T) { var log bytes.Buffer cmd := kubeconfigCommand(kubeconfigDeps{ + getenv: func(key string) string { + return tt.env[key] + }, getPathToSelf: func() (string, error) { if tt.getPathToSelfErr != nil { return "", tt.getPathToSelfErr diff --git a/cmd/pinniped/cmd/whoami.go b/cmd/pinniped/cmd/whoami.go index e4aea18ae..c6c30403f 100644 --- a/cmd/pinniped/cmd/whoami.go +++ b/cmd/pinniped/cmd/whoami.go @@ -24,9 +24,21 @@ import ( "go.pinniped.dev/internal/here" ) +type whoamiDeps struct { + getenv func(key string) string + getClientset getConciergeClientsetFunc +} + +func whoamiRealDeps() whoamiDeps { + return whoamiDeps{ + getenv: os.Getenv, + getClientset: getRealConciergeClientset, + } +} + //nolint:gochecknoinits func init() { - rootCmd.AddCommand(newWhoamiCommand(getRealConciergeClientset)) + rootCmd.AddCommand(newWhoamiCommand(whoamiRealDeps())) } type whoamiFlags struct { @@ -44,7 +56,7 @@ type clusterInfo struct { url string } -func newWhoamiCommand(getClientset getConciergeClientsetFunc) *cobra.Command { +func newWhoamiCommand(deps whoamiDeps) *cobra.Command { cmd := &cobra.Command{ Args: cobra.NoArgs, // do not accept positional arguments for this command Use: "whoami", @@ -56,21 +68,21 @@ func newWhoamiCommand(getClientset getConciergeClientsetFunc) *cobra.Command { // flags f := cmd.Flags() f.StringVarP(&flags.outputFormat, "output", "o", "text", "Output format (e.g., 'yaml', 'json', 'text')") - f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file") + f.StringVar(&flags.kubeconfigPath, "kubeconfig", deps.getenv("KUBECONFIG"), "Path to kubeconfig file") f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)") f.StringVar(&flags.apiGroupSuffix, "api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") f.DurationVar(&flags.timeout, "timeout", 0, "Timeout for the WhoAmI API request (default: 0, meaning no timeout)") cmd.RunE = func(cmd *cobra.Command, _ []string) error { - return runWhoami(cmd.OutOrStdout(), getClientset, flags) + return runWhoami(cmd.OutOrStdout(), deps, flags) } return cmd } -func runWhoami(output io.Writer, getClientset getConciergeClientsetFunc, flags *whoamiFlags) error { +func runWhoami(output io.Writer, deps whoamiDeps, flags *whoamiFlags) error { clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride) - clientset, err := getClientset(clientConfig, flags.apiGroupSuffix) + clientset, err := deps.getClientset(clientConfig, flags.apiGroupSuffix) if err != nil { return fmt.Errorf("could not configure Kubernetes client: %w", err) } diff --git a/cmd/pinniped/cmd/whoami_test.go b/cmd/pinniped/cmd/whoami_test.go index 408f34879..c01ef99fb 100644 --- a/cmd/pinniped/cmd/whoami_test.go +++ b/cmd/pinniped/cmd/whoami_test.go @@ -5,6 +5,7 @@ package cmd import ( "bytes" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -21,9 +22,25 @@ import ( ) func TestWhoami(t *testing.T) { + helpOutputFormatString := here.Doc(` + Print information about the current user + + Usage: + whoami [flags] + + Flags: + --api-group-suffix string Concierge API group suffix (default "pinniped.dev") + -h, --help help for whoami + --kubeconfig string Path to kubeconfig file%s + --kubeconfig-context string Kubeconfig context name (default: current active context) + -o, --output string Output format (e.g., 'yaml', 'json', 'text') (default "text") + --timeout duration Timeout for the WhoAmI API request (default: 0, meaning no timeout) + `) + tests := []struct { name string args []string + env map[string]string groupsOverride []string gettingClientsetErr error callingAPIErr error @@ -31,22 +48,17 @@ func TestWhoami(t *testing.T) { wantStdout, wantStderr string }{ { - name: "help flag", - args: []string{"--help"}, - wantStdout: here.Doc(` - Print information about the current user - - Usage: - whoami [flags] - - Flags: - --api-group-suffix string Concierge API group suffix (default "pinniped.dev") - -h, --help help for whoami - --kubeconfig string Path to kubeconfig file - --kubeconfig-context string Kubeconfig context name (default: current active context) - -o, --output string Output format (e.g., 'yaml', 'json', 'text') (default "text") - --timeout duration Timeout for the WhoAmI API request (default: 0, meaning no timeout) - `), + name: "help flag passed", + args: []string{"--help"}, + wantStdout: fmt.Sprintf(helpOutputFormatString, ""), + }, + { + name: "help flag passed with KUBECONFIG env var set", + env: map[string]string{ + "KUBECONFIG": "/path/to/kubeconfig", + }, + args: []string{"--help"}, + wantStdout: fmt.Sprintf(helpOutputFormatString, ` (default "/path/to/kubeconfig")`), }, { name: "text output", @@ -306,7 +318,12 @@ func TestWhoami(t *testing.T) { }) return clientset, nil } - cmd := newWhoamiCommand(getClientset) + cmd := newWhoamiCommand(whoamiDeps{ + getenv: func(key string) string { + return test.env[key] + }, + getClientset: getClientset, + }) stdout, stderr := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}) cmd.SetOut(stdout)