Add IDP selection to get-kubeconfig command.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
Matt Moyer 2020-09-21 17:41:30 -05:00
parent 481308215d
commit 07f0181fa3
No known key found for this signature in database
GPG Key ID: EAE88AD172C5AE2D
2 changed files with 127 additions and 6 deletions

View File

@ -37,6 +37,8 @@ type getKubeConfigFlags struct {
kubeconfig string
contextOverride string
namespace string
idpName string
idpType string
}
type getKubeConfigCommand struct {
@ -90,6 +92,8 @@ func (c *getKubeConfigCommand) Command() *cobra.Command {
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.idpType, "idp-type", c.flags.idpType, "Identity provider type (e.g., 'webhook')")
cmd.Flags().StringVar(&c.flags.idpName, "idp-name", c.flags.idpType, "Identity provider name")
return cmd
}
@ -115,6 +119,14 @@ func (c *getKubeConfigCommand) run(cmd *cobra.Command, args []string) error {
return err
}
idpType, idpName := c.flags.idpType, c.flags.idpName
if idpType == "" || idpName == "" {
idpType, idpName, err = getDefaultIDP(clientset, c.flags.namespace)
if err != nil {
return err
}
}
credentialIssuerConfig, err := fetchPinnipedCredentialIssuerConfig(clientset, c.flags.namespace)
if err != nil {
return err
@ -134,7 +146,7 @@ func (c *getKubeConfigCommand) run(cmd *cobra.Command, args []string) error {
return err
}
config := newPinnipedKubeconfig(v1Cluster, fullPathToSelf, c.flags.token, c.flags.namespace)
config := newPinnipedKubeconfig(v1Cluster, fullPathToSelf, c.flags.token, c.flags.namespace, idpType, idpName)
err = writeConfigAsYAML(cmd.OutOrStdout(), config)
if err != nil {
@ -159,6 +171,45 @@ func issueWarningForNonMatchingServerOrCA(v1Cluster v1.Cluster, credentialIssuer
return nil
}
type noIDPError struct{ Namespace string }
func (e noIDPError) Error() string {
return fmt.Sprintf(`no identity providers were found in namespace %q`, e.Namespace)
}
type indeterminateIDPError struct{ Namespace string }
func (e indeterminateIDPError) Error() string {
return fmt.Sprintf(
`multiple identity providers were found in namespace %q, so --pinniped-idp-name/--pinniped-idp-type must be specified`,
e.Namespace,
)
}
func getDefaultIDP(clientset pinnipedclientset.Interface, namespace string) (string, string, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc()
webhooks, err := clientset.IDPV1alpha1().WebhookIdentityProviders(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return "", "", err
}
type ref struct{ idpType, idpName string }
idps := make([]ref, 0, len(webhooks.Items))
for _, webhook := range webhooks.Items {
idps = append(idps, ref{idpType: "webhook", idpName: webhook.Name})
}
if len(idps) == 0 {
return "", "", noIDPError{namespace}
}
if len(idps) > 1 {
return "", "", indeterminateIDPError{namespace}
}
return idps[0].idpType, idps[0].idpName, nil
}
func fetchPinnipedCredentialIssuerConfig(clientset pinnipedclientset.Interface, pinnipedInstallationNamespace string) (*configv1alpha1.CredentialIssuerConfig, error) {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
defer cancelFunc()
@ -229,7 +280,7 @@ func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Con
return v1Cluster, nil
}
func newPinnipedKubeconfig(v1Cluster v1.Cluster, fullPathToSelf string, token string, namespace string) v1.Config {
func newPinnipedKubeconfig(v1Cluster v1.Cluster, fullPathToSelf string, token string, namespace string, idpType string, idpName string) v1.Config {
clusterName := "pinniped-cluster"
userName := "pinniped-user"
@ -275,6 +326,14 @@ func newPinnipedKubeconfig(v1Cluster v1.Cluster, fullPathToSelf string, token st
Name: "PINNIPED_TOKEN",
Value: token,
},
{
Name: "PINNIPED_IDP_TYPE",
Value: idpType,
},
{
Name: "PINNIPED_IDP_NAME",
Value: idpName,
},
},
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
InstallHint: "The Pinniped CLI is required to authenticate to the current cluster.\n" +

View File

@ -18,6 +18,7 @@ import (
coretesting "k8s.io/client-go/testing"
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/config/v1alpha1"
idpv1alpha "go.pinniped.dev/generated/1.19/apis/idp/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned"
pinnipedfake "go.pinniped.dev/generated/1.19/client/clientset/versioned/fake"
"go.pinniped.dev/internal/here"
@ -30,6 +31,8 @@ var (
Flags:
-h, --help help for get-kubeconfig
--idp-name string Identity provider name
--idp-type string Identity provider type (e.g., 'webhook')
--kubeconfig string Path to the kubeconfig file
--kubeconfig-context string Kubeconfig context override
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
@ -59,6 +62,8 @@ var (
Flags:
-h, --help help for get-kubeconfig
--idp-name string Identity provider name
--idp-type string Identity provider type (e.g., 'webhook')
--kubeconfig string Path to the kubeconfig file
--kubeconfig-context string Kubeconfig context override
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
@ -118,6 +123,8 @@ type expectedKubeconfigYAML struct {
pinnipedEndpoint string
pinnipedCABundle string
namespace string
idpType string
idpName string
}
func (e expectedKubeconfigYAML) String() string {
@ -153,10 +160,14 @@ func (e expectedKubeconfigYAML) String() string {
value: %s
- name: PINNIPED_TOKEN
value: %s
- name: PINNIPED_IDP_TYPE
value: %s
- name: PINNIPED_IDP_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.clusterCAData, e.clusterServer, e.command, e.pinnipedEndpoint, e.pinnipedCABundle, e.namespace, e.token, e.idpType, e.idpName)
}
func newCredentialIssuerConfig(name, namespace, server, certificateAuthorityData string) *configv1alpha1.CredentialIssuerConfig {
@ -212,6 +223,46 @@ func TestRun(t *testing.T) {
},
wantError: "some error configuring clientset",
},
{
name: "fail to get IDPs",
mocks: func(cmd *getKubeConfigCommand) {
cmd.flags.idpName = ""
cmd.flags.idpType = ""
clientset := pinnipedfake.NewSimpleClientset()
clientset.PrependReactor("*", "*", func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, nil, fmt.Errorf("some error getting IDPs")
})
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
return clientset, nil
}
},
wantError: "some error getting IDPs",
},
{
name: "zero IDPs",
mocks: func(cmd *getKubeConfigCommand) {
cmd.flags.idpName = ""
cmd.flags.idpType = ""
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
return pinnipedfake.NewSimpleClientset(), nil
}
},
wantError: `no identity providers were found in namespace "test-namespace"`,
},
{
name: "multiple IDPs",
mocks: func(cmd *getKubeConfigCommand) {
cmd.flags.idpName = ""
cmd.flags.idpType = ""
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
return pinnipedfake.NewSimpleClientset(
&idpv1alpha.WebhookIdentityProvider{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "webhook-one"}},
&idpv1alpha.WebhookIdentityProvider{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "webhook-two"}},
), nil
}
},
wantError: `multiple identity providers were found in namespace "test-namespace", so --pinniped-idp-name/--pinniped-idp-type must be specified`,
},
{
name: "fail to get CredentialIssuerConfigs",
mocks: func(cmd *getKubeConfigCommand) {
@ -286,14 +337,21 @@ func TestRun(t *testing.T) {
pinnipedEndpoint: "https://fake-server-url-value",
pinnipedCABundle: "fake-certificate-authority-data-value",
namespace: "test-namespace",
idpType: "test-idp-type",
idpName: "test-idp-name",
}.String(),
},
{
name: "success using local CA data",
name: "success using local CA data and discovered IDP",
mocks: func(cmd *getKubeConfigCommand) {
cic := newCredentialIssuerConfig("pinniped-config", "test-namespace", "https://example.com", "test-ca")
cmd.flags.idpName = ""
cmd.flags.idpType = ""
cmd.kubeClientCreator = func(_ *rest.Config) (pinnipedclientset.Interface, error) {
return pinnipedfake.NewSimpleClientset(cic), nil
return pinnipedfake.NewSimpleClientset(
&idpv1alpha.WebhookIdentityProvider{ObjectMeta: metav1.ObjectMeta{Namespace: "test-namespace", Name: "discovered-idp"}},
newCredentialIssuerConfig("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 CredentialIssuerConfig on the cluster. Using local kubeconfig values.`,
@ -305,6 +363,8 @@ func TestRun(t *testing.T) {
pinnipedEndpoint: "https://fake-server-url-value",
pinnipedCABundle: "fake-certificate-authority-data-value",
namespace: "test-namespace",
idpType: "webhook",
idpName: "discovered-idp",
}.String(),
},
}
@ -317,6 +377,8 @@ func TestRun(t *testing.T) {
c := newGetKubeConfigCommand()
c.flags.token = "test-token"
c.flags.namespace = "test-namespace"
c.flags.idpName = "test-idp-name"
c.flags.idpType = "test-idp-type"
c.getPathToSelf = func() (string, error) { return "/path/to/pinniped", nil }
c.flags.kubeconfig = "./testdata/kubeconfig.yaml"
tt.mocks(c)