Remove pinniped get-kubeconfig
CLI subcommand.
Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
3b5f00439c
commit
dfbb5b60de
@ -1,346 +0,0 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ghodss/yaml"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
"k8s.io/client-go/tools/clientcmd"
|
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
|
||||||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
|
||||||
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/config/v1alpha1"
|
|
||||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
|
|
||||||
"go.pinniped.dev/internal/constable"
|
|
||||||
"go.pinniped.dev/internal/here"
|
|
||||||
"go.pinniped.dev/internal/plog"
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint: gochecknoinits
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(newGetKubeConfigCommand().Command())
|
|
||||||
}
|
|
||||||
|
|
||||||
type getKubeConfigFlags struct {
|
|
||||||
token string
|
|
||||||
kubeconfig string
|
|
||||||
contextOverride string
|
|
||||||
namespace string
|
|
||||||
authenticatorName string
|
|
||||||
authenticatorType string
|
|
||||||
}
|
|
||||||
|
|
||||||
type getKubeConfigCommand struct {
|
|
||||||
flags getKubeConfigFlags
|
|
||||||
// Test mocking points
|
|
||||||
getPathToSelf func() (string, error)
|
|
||||||
kubeClientCreator func(restConfig *rest.Config) (pinnipedclientset.Interface, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGetKubeConfigCommand() *getKubeConfigCommand {
|
|
||||||
return &getKubeConfigCommand{
|
|
||||||
flags: getKubeConfigFlags{
|
|
||||||
namespace: "pinniped-concierge",
|
|
||||||
},
|
|
||||||
getPathToSelf: os.Executable,
|
|
||||||
kubeClientCreator: func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedclientset.NewForConfig(restConfig)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *getKubeConfigCommand) Command() *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
RunE: c.run,
|
|
||||||
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
|
||||||
Use: "get-kubeconfig",
|
|
||||||
Short: "Print a kubeconfig for authenticating into a cluster via Pinniped",
|
|
||||||
Long: 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.
|
|
||||||
|
|
||||||
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
|
|
||||||
`),
|
|
||||||
}
|
|
||||||
cmd.Flags().StringVar(&c.flags.token, "token", "", "Credential to include in the resulting kubeconfig output (Required)")
|
|
||||||
cmd.Flags().StringVar(&c.flags.kubeconfig, "kubeconfig", c.flags.kubeconfig, "Path to the kubeconfig file")
|
|
||||||
cmd.Flags().StringVar(&c.flags.contextOverride, "kubeconfig-context", c.flags.contextOverride, "Kubeconfig context override")
|
|
||||||
cmd.Flags().StringVar(&c.flags.namespace, "pinniped-namespace", c.flags.namespace, "Namespace in which Pinniped was installed")
|
|
||||||
cmd.Flags().StringVar(&c.flags.authenticatorType, "authenticator-type", c.flags.authenticatorType, "Authenticator type (e.g., 'webhook', 'jwt')")
|
|
||||||
cmd.Flags().StringVar(&c.flags.authenticatorName, "authenticator-name", c.flags.authenticatorType, "Authenticator name")
|
|
||||||
mustMarkRequired(cmd, "token")
|
|
||||||
plog.RemoveKlogGlobalFlags()
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *getKubeConfigCommand) run(cmd *cobra.Command, args []string) error {
|
|
||||||
fullPathToSelf, err := c.getPathToSelf()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not find path to self: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig := newClientConfig(c.flags.kubeconfig, c.flags.contextOverride)
|
|
||||||
|
|
||||||
currentKubeConfig, err := clientConfig.RawConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
restConfig, err := clientConfig.ClientConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
clientset, err := c.kubeClientCreator(restConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticatorType, authenticatorName := c.flags.authenticatorType, c.flags.authenticatorName
|
|
||||||
if authenticatorType == "" || authenticatorName == "" {
|
|
||||||
authenticatorType, authenticatorName, err = getDefaultAuthenticator(clientset, c.flags.namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
credentialIssuer, err := fetchPinnipedCredentialIssuer(clientset, c.flags.namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if credentialIssuer.Status.KubeConfigInfo == nil {
|
|
||||||
return constable.Error(`CredentialIssuer "pinniped-config" was missing KubeConfigInfo`)
|
|
||||||
}
|
|
||||||
|
|
||||||
v1Cluster, err := copyCurrentClusterFromExistingKubeConfig(currentKubeConfig, c.flags.contextOverride)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = issueWarningForNonMatchingServerOrCA(v1Cluster, credentialIssuer, cmd.ErrOrStderr())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
config := newPinnipedKubeconfig(v1Cluster, fullPathToSelf, c.flags.token, c.flags.namespace, authenticatorType, authenticatorName)
|
|
||||||
|
|
||||||
err = writeConfigAsYAML(cmd.OutOrStdout(), config)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func issueWarningForNonMatchingServerOrCA(v1Cluster v1.Cluster, credentialIssuer *configv1alpha1.CredentialIssuer, warningsWriter io.Writer) error {
|
|
||||||
credentialIssuerCA, err := base64.StdEncoding.DecodeString(credentialIssuer.Status.KubeConfigInfo.CertificateAuthorityData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if v1Cluster.Server != credentialIssuer.Status.KubeConfigInfo.Server ||
|
|
||||||
!bytes.Equal(v1Cluster.CertificateAuthorityData, credentialIssuerCA) {
|
|
||||||
_, err := warningsWriter.Write([]byte("WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuer on the cluster. Using local kubeconfig values.\n"))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("output write error: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type noAuthenticatorError struct{ Namespace string }
|
|
||||||
|
|
||||||
func (e noAuthenticatorError) Error() string {
|
|
||||||
return fmt.Sprintf(`no authenticators were found in namespace %q`, e.Namespace)
|
|
||||||
}
|
|
||||||
|
|
||||||
type indeterminateAuthenticatorError struct{ Namespace string }
|
|
||||||
|
|
||||||
func (e indeterminateAuthenticatorError) Error() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
`multiple authenticators were found in namespace %q, so --authenticator-name/--authenticator-type must be specified`,
|
|
||||||
e.Namespace,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDefaultAuthenticator(clientset pinnipedclientset.Interface, namespace string) (string, string, error) {
|
|
||||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
|
||||||
defer cancelFunc()
|
|
||||||
|
|
||||||
webhooks, err := clientset.AuthenticationV1alpha1().WebhookAuthenticators(namespace).List(ctx, metav1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
type ref struct{ authenticatorType, authenticatorName string }
|
|
||||||
authenticators := make([]ref, 0, len(webhooks.Items))
|
|
||||||
for _, webhook := range webhooks.Items {
|
|
||||||
authenticators = append(authenticators, ref{authenticatorType: "webhook", authenticatorName: webhook.Name})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(authenticators) == 0 {
|
|
||||||
return "", "", noAuthenticatorError{namespace}
|
|
||||||
}
|
|
||||||
if len(authenticators) > 1 {
|
|
||||||
return "", "", indeterminateAuthenticatorError{namespace}
|
|
||||||
}
|
|
||||||
return authenticators[0].authenticatorType, authenticators[0].authenticatorName, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchPinnipedCredentialIssuer(clientset pinnipedclientset.Interface, pinnipedInstallationNamespace string) (*configv1alpha1.CredentialIssuer, error) {
|
|
||||||
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
|
||||||
defer cancelFunc()
|
|
||||||
|
|
||||||
credentialIssuers, err := clientset.ConfigV1alpha1().CredentialIssuers(pinnipedInstallationNamespace).List(ctx, metav1.ListOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(credentialIssuers.Items) == 0 {
|
|
||||||
return nil, constable.Error(fmt.Sprintf(
|
|
||||||
`No CredentialIssuer was found in namespace "%s". Is Pinniped installed on this cluster in namespace "%s"?`,
|
|
||||||
pinnipedInstallationNamespace,
|
|
||||||
pinnipedInstallationNamespace,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(credentialIssuers.Items) > 1 {
|
|
||||||
return nil, constable.Error(fmt.Sprintf(
|
|
||||||
`More than one CredentialIssuer was found in namespace "%s"`,
|
|
||||||
pinnipedInstallationNamespace,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &credentialIssuers.Items[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClientConfig(kubeconfigPathOverride string, currentContextName string) clientcmd.ClientConfig {
|
|
||||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
|
||||||
loadingRules.ExplicitPath = kubeconfigPathOverride
|
|
||||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{
|
|
||||||
CurrentContext: currentContextName,
|
|
||||||
})
|
|
||||||
return clientConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeConfigAsYAML(outputWriter io.Writer, config v1.Config) error {
|
|
||||||
output, err := yaml.Marshal(&config)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("YAML serialization error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = outputWriter.Write(output)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("output write error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Config, currentContextNameOverride string) (v1.Cluster, error) {
|
|
||||||
v1Cluster := v1.Cluster{}
|
|
||||||
|
|
||||||
contextName := currentKubeConfig.CurrentContext
|
|
||||||
if currentContextNameOverride != "" {
|
|
||||||
contextName = currentContextNameOverride
|
|
||||||
}
|
|
||||||
|
|
||||||
err := v1.Convert_api_Cluster_To_v1_Cluster(
|
|
||||||
currentKubeConfig.Clusters[currentKubeConfig.Contexts[contextName].Cluster],
|
|
||||||
&v1Cluster,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return v1.Cluster{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return v1Cluster, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPinnipedKubeconfig(v1Cluster v1.Cluster, fullPathToSelf string, token string, namespace string, authenticatorType string, authenticatorName string) v1.Config {
|
|
||||||
clusterName := "pinniped-cluster"
|
|
||||||
userName := "pinniped-user"
|
|
||||||
|
|
||||||
return v1.Config{
|
|
||||||
Kind: "Config",
|
|
||||||
APIVersion: v1.SchemeGroupVersion.Version,
|
|
||||||
Preferences: v1.Preferences{},
|
|
||||||
Clusters: []v1.NamedCluster{
|
|
||||||
{
|
|
||||||
Name: clusterName,
|
|
||||||
Cluster: v1Cluster,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Contexts: []v1.NamedContext{
|
|
||||||
{
|
|
||||||
Name: clusterName,
|
|
||||||
Context: v1.Context{
|
|
||||||
Cluster: clusterName,
|
|
||||||
AuthInfo: userName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
AuthInfos: []v1.NamedAuthInfo{
|
|
||||||
{
|
|
||||||
Name: userName,
|
|
||||||
AuthInfo: v1.AuthInfo{
|
|
||||||
Exec: &v1.ExecConfig{
|
|
||||||
Command: fullPathToSelf,
|
|
||||||
Args: []string{"exchange-credential"},
|
|
||||||
Env: []v1.ExecEnvVar{
|
|
||||||
{
|
|
||||||
Name: "PINNIPED_K8S_API_ENDPOINT",
|
|
||||||
Value: v1Cluster.Server,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "PINNIPED_CA_BUNDLE",
|
|
||||||
Value: string(v1Cluster.CertificateAuthorityData)},
|
|
||||||
{
|
|
||||||
Name: "PINNIPED_NAMESPACE",
|
|
||||||
Value: namespace,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "PINNIPED_TOKEN",
|
|
||||||
Value: token,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "PINNIPED_AUTHENTICATOR_TYPE",
|
|
||||||
Value: authenticatorType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "PINNIPED_AUTHENTICATOR_NAME",
|
|
||||||
Value: authenticatorName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
|
|
||||||
InstallHint: "The Pinniped CLI is required to authenticate to the current cluster.\n" +
|
|
||||||
"For more information, please visit https://pinniped.dev",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CurrentContext: clusterName,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,399 +0,0 @@
|
|||||||
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
coretesting "k8s.io/client-go/testing"
|
|
||||||
|
|
||||||
authv1alpha "go.pinniped.dev/generated/1.19/apis/concierge/authentication/v1alpha1"
|
|
||||||
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/config/v1alpha1"
|
|
||||||
pinnipedclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
|
|
||||||
pinnipedfake "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned/fake"
|
|
||||||
"go.pinniped.dev/internal/here"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
knownGoodUsageForGetKubeConfig = here.Doc(`
|
|
||||||
Usage:
|
|
||||||
get-kubeconfig [flags]
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
--authenticator-name string Authenticator name
|
|
||||||
--authenticator-type string Authenticator type (e.g., 'webhook', 'jwt')
|
|
||||||
-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-concierge")
|
|
||||||
--token string Credential to include in the resulting kubeconfig output (Required)
|
|
||||||
|
|
||||||
`)
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
--authenticator-name string Authenticator name
|
|
||||||
--authenticator-type string Authenticator type (e.g., 'webhook', 'jwt')
|
|
||||||
-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-concierge")
|
|
||||||
--token string Credential to include in the resulting kubeconfig output (Required)
|
|
||||||
`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewGetKubeConfigCmd(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
wantError bool
|
|
||||||
wantStdout string
|
|
||||||
wantStderr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "help flag passed",
|
|
||||||
args: []string{"--help"},
|
|
||||||
wantStdout: knownGoodHelpForGetKubeConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing required flag",
|
|
||||||
args: []string{},
|
|
||||||
wantError: true,
|
|
||||||
wantStdout: `Error: required flag(s) "token" not set` + "\n" + knownGoodUsageForGetKubeConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
cmd := newGetKubeConfigCommand().Command()
|
|
||||||
require.NotNil(t, cmd)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.SetOut(&stdout)
|
|
||||||
cmd.SetErr(&stderr)
|
|
||||||
cmd.SetArgs(tt.args)
|
|
||||||
err := cmd.Execute()
|
|
||||||
if tt.wantError {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
|
|
||||||
require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type expectedKubeconfigYAML struct {
|
|
||||||
clusterCAData string
|
|
||||||
clusterServer string
|
|
||||||
command string
|
|
||||||
token string
|
|
||||||
pinnipedEndpoint string
|
|
||||||
pinnipedCABundle string
|
|
||||||
namespace string
|
|
||||||
authenticatorType string
|
|
||||||
authenticatorName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e expectedKubeconfigYAML) String() string {
|
|
||||||
return here.Docf(`
|
|
||||||
apiVersion: v1
|
|
||||||
clusters:
|
|
||||||
- cluster:
|
|
||||||
certificate-authority-data: %s
|
|
||||||
server: %s
|
|
||||||
name: pinniped-cluster
|
|
||||||
contexts:
|
|
||||||
- context:
|
|
||||||
cluster: pinniped-cluster
|
|
||||||
user: pinniped-user
|
|
||||||
name: pinniped-cluster
|
|
||||||
current-context: pinniped-cluster
|
|
||||||
kind: Config
|
|
||||||
preferences: {}
|
|
||||||
users:
|
|
||||||
- name: pinniped-user
|
|
||||||
user:
|
|
||||||
exec:
|
|
||||||
apiVersion: client.authentication.k8s.io/v1beta1
|
|
||||||
args:
|
|
||||||
- exchange-credential
|
|
||||||
command: %s
|
|
||||||
env:
|
|
||||||
- name: PINNIPED_K8S_API_ENDPOINT
|
|
||||||
value: %s
|
|
||||||
- name: PINNIPED_CA_BUNDLE
|
|
||||||
value: %s
|
|
||||||
- name: PINNIPED_NAMESPACE
|
|
||||||
value: %s
|
|
||||||
- name: PINNIPED_TOKEN
|
|
||||||
value: %s
|
|
||||||
- name: PINNIPED_AUTHENTICATOR_TYPE
|
|
||||||
value: %s
|
|
||||||
- name: PINNIPED_AUTHENTICATOR_NAME
|
|
||||||
value: %s
|
|
||||||
installHint: |-
|
|
||||||
The Pinniped CLI is required to authenticate to the current cluster.
|
|
||||||
For more information, please visit https://pinniped.dev
|
|
||||||
`, e.clusterCAData, e.clusterServer, e.command, e.pinnipedEndpoint, e.pinnipedCABundle, e.namespace, e.token, e.authenticatorType, e.authenticatorName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCredentialIssuer(name, namespace, server, certificateAuthorityData string) *configv1alpha1.CredentialIssuer {
|
|
||||||
return &configv1alpha1.CredentialIssuer{
|
|
||||||
TypeMeta: metav1.TypeMeta{
|
|
||||||
Kind: "CredentialIssuer",
|
|
||||||
APIVersion: configv1alpha1.SchemeGroupVersion.String(),
|
|
||||||
},
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: name,
|
|
||||||
Namespace: namespace,
|
|
||||||
},
|
|
||||||
Status: configv1alpha1.CredentialIssuerStatus{
|
|
||||||
KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{
|
|
||||||
Server: server,
|
|
||||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(certificateAuthorityData)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRun(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
mocks func(*getKubeConfigCommand)
|
|
||||||
wantError string
|
|
||||||
wantStdout string
|
|
||||||
wantStderr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "failure to get path to self",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.getPathToSelf = func() (string, error) {
|
|
||||||
return "", fmt.Errorf("some error getting path to self")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: "could not find path to self: some error getting path to self",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "kubeconfig does not exist",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.flags.kubeconfig = "./testdata/does-not-exist.yaml"
|
|
||||||
},
|
|
||||||
wantError: "stat ./testdata/does-not-exist.yaml: no such file or directory",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fail to get client",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return nil, fmt.Errorf("some error configuring clientset")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: "some error configuring clientset",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fail to get authenticators",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.flags.authenticatorName = ""
|
|
||||||
cmd.flags.authenticatorType = ""
|
|
||||||
clientset := pinnipedfake.NewSimpleClientset()
|
|
||||||
clientset.PrependReactor("*", "*", func(_ coretesting.Action) (bool, runtime.Object, error) {
|
|
||||||
return true, nil, fmt.Errorf("some error getting authenticators")
|
|
||||||
})
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return clientset, nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: "some error getting authenticators",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero authenticators",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.flags.authenticatorName = ""
|
|
||||||
cmd.flags.authenticatorType = ""
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: `no authenticators were found in namespace "test-namespace"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple authenticators",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.flags.authenticatorName = ""
|
|
||||||
cmd.flags.authenticatorType = ""
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(
|
|
||||||
&authv1alpha.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "webhook-one"}},
|
|
||||||
&authv1alpha.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "webhook-two"}},
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: `multiple authenticators were found in namespace "test-namespace", so --authenticator-name/--authenticator-type must be specified`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fail to get CredentialIssuers",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
clientset := pinnipedfake.NewSimpleClientset()
|
|
||||||
clientset.PrependReactor("*", "*", func(_ coretesting.Action) (bool, runtime.Object, error) {
|
|
||||||
return true, nil, fmt.Errorf("some error getting CredentialIssuers")
|
|
||||||
})
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return clientset, nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: "some error getting CredentialIssuers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "zero CredentialIssuers found",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(
|
|
||||||
newCredentialIssuer("pinniped-config-1", "not-the-test-namespace", "", ""),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: `No CredentialIssuer was found in namespace "test-namespace". Is Pinniped installed on this cluster in namespace "test-namespace"?`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple CredentialIssuers found",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(
|
|
||||||
newCredentialIssuer("pinniped-config-1", "test-namespace", "", ""),
|
|
||||||
newCredentialIssuer("pinniped-config-2", "test-namespace", "", ""),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: `More than one CredentialIssuer was found in namespace "test-namespace"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "CredentialIssuer missing KubeConfigInfo",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
ci := newCredentialIssuer("pinniped-config", "test-namespace", "", "")
|
|
||||||
ci.Status.KubeConfigInfo = nil
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(ci), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: `CredentialIssuer "pinniped-config" was missing KubeConfigInfo`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "KubeConfigInfo has invalid base64",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
ci := newCredentialIssuer("pinniped-config", "test-namespace", "https://example.com", "")
|
|
||||||
ci.Status.KubeConfigInfo.CertificateAuthorityData = "invalid-base64-test-ca"
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(ci), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantError: `illegal base64 data at input byte 7`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success using remote CA data",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
ci := newCredentialIssuer("pinniped-config", "test-namespace", "https://fake-server-url-value", "fake-certificate-authority-data-value")
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(ci), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantStdout: expectedKubeconfigYAML{
|
|
||||||
clusterCAData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==",
|
|
||||||
clusterServer: "https://fake-server-url-value",
|
|
||||||
command: "/path/to/pinniped",
|
|
||||||
token: "test-token",
|
|
||||||
pinnipedEndpoint: "https://fake-server-url-value",
|
|
||||||
pinnipedCABundle: "fake-certificate-authority-data-value",
|
|
||||||
namespace: "test-namespace",
|
|
||||||
authenticatorType: "test-authenticator-type",
|
|
||||||
authenticatorName: "test-authenticator-name",
|
|
||||||
}.String(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success using local CA data and discovered authenticator",
|
|
||||||
mocks: func(cmd *getKubeConfigCommand) {
|
|
||||||
cmd.flags.authenticatorName = ""
|
|
||||||
cmd.flags.authenticatorType = ""
|
|
||||||
|
|
||||||
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
|
|
||||||
return pinnipedfake.NewSimpleClientset(
|
|
||||||
&authv1alpha.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "discovered-authenticator"}},
|
|
||||||
newCredentialIssuer("pinniped-config", "test-namespace", "https://example.com", "test-ca"),
|
|
||||||
), nil
|
|
||||||
}
|
|
||||||
},
|
|
||||||
wantStderr: `WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuer on the cluster. Using local kubeconfig values.`,
|
|
||||||
wantStdout: expectedKubeconfigYAML{
|
|
||||||
clusterCAData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==",
|
|
||||||
clusterServer: "https://fake-server-url-value",
|
|
||||||
command: "/path/to/pinniped",
|
|
||||||
token: "test-token",
|
|
||||||
pinnipedEndpoint: "https://fake-server-url-value",
|
|
||||||
pinnipedCABundle: "fake-certificate-authority-data-value",
|
|
||||||
namespace: "test-namespace",
|
|
||||||
authenticatorType: "webhook",
|
|
||||||
authenticatorName: "discovered-authenticator",
|
|
||||||
}.String(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Start with a default getKubeConfigCommand, set some defaults, then apply any mocks.
|
|
||||||
c := newGetKubeConfigCommand()
|
|
||||||
c.flags.token = "test-token"
|
|
||||||
c.flags.namespace = "test-namespace"
|
|
||||||
c.flags.authenticatorName = "test-authenticator-name"
|
|
||||||
c.flags.authenticatorType = "test-authenticator-type"
|
|
||||||
c.getPathToSelf = func() (string, error) { return "/path/to/pinniped", nil }
|
|
||||||
c.flags.kubeconfig = "./testdata/kubeconfig.yaml"
|
|
||||||
tt.mocks(c)
|
|
||||||
|
|
||||||
cmd := &cobra.Command{}
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.SetOut(&stdout)
|
|
||||||
cmd.SetErr(&stderr)
|
|
||||||
cmd.SetArgs([]string{})
|
|
||||||
err := c.run(cmd, []string{})
|
|
||||||
if tt.wantError != "" {
|
|
||||||
require.EqualError(t, err, tt.wantError)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
require.Equal(t, strings.TrimSpace(tt.wantStdout), strings.TrimSpace(stdout.String()), "unexpected stdout")
|
|
||||||
require.Equal(t, strings.TrimSpace(tt.wantStderr), strings.TrimSpace(stderr.String()), "unexpected stderr")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
1
go.mod
1
go.mod
@ -7,7 +7,6 @@ require (
|
|||||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/ghodss/yaml v1.0.0
|
|
||||||
github.com/go-logr/logr v0.2.1
|
github.com/go-logr/logr v0.2.1
|
||||||
github.com/go-logr/stdr v0.2.0
|
github.com/go-logr/stdr v0.2.0
|
||||||
github.com/gofrs/flock v0.8.0
|
github.com/gofrs/flock v0.8.0
|
||||||
|
Loading…
Reference in New Issue
Block a user