From 07f0181fa3b92ed77e2ff23c69e7a9bb9ff5d190 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Mon, 21 Sep 2020 17:41:30 -0500 Subject: [PATCH] Add IDP selection to get-kubeconfig command. Signed-off-by: Matt Moyer --- cmd/pinniped/cmd/get_kubeconfig.go | 63 +++++++++++++++++++++- cmd/pinniped/cmd/get_kubeconfig_test.go | 70 +++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/cmd/pinniped/cmd/get_kubeconfig.go b/cmd/pinniped/cmd/get_kubeconfig.go index 7992f087..0660c556 100644 --- a/cmd/pinniped/cmd/get_kubeconfig.go +++ b/cmd/pinniped/cmd/get_kubeconfig.go @@ -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" + diff --git a/cmd/pinniped/cmd/get_kubeconfig_test.go b/cmd/pinniped/cmd/get_kubeconfig_test.go index d8bb7232..7834e1b4 100644 --- a/cmd/pinniped/cmd/get_kubeconfig_test.go +++ b/cmd/pinniped/cmd/get_kubeconfig_test.go @@ -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)