From 4379d2772c0b09c1eddbf9c5324f8431ea81f3fc Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 14 Sep 2020 19:07:18 -0700 Subject: [PATCH] CLI `get-kubeconfig` command reads kubeconfig and CredentialIssuerConfig --- cmd/pinniped/cmd/get_kubeconfig.go | 274 ++++++++--- cmd/pinniped/cmd/get_kubeconfig_test.go | 430 ++++++++++++++++-- cmd/pinniped/cmd/testdata/kubeconfig.yaml | 31 ++ ...eate_or_update_credential_issuer_config.go | 4 +- internal/controller/issuerconfig/publisher.go | 10 +- 5 files changed, 652 insertions(+), 97 deletions(-) create mode 100644 cmd/pinniped/cmd/testdata/kubeconfig.yaml diff --git a/cmd/pinniped/cmd/get_kubeconfig.go b/cmd/pinniped/cmd/get_kubeconfig.go index 7bceadc18..a92a4f969 100644 --- a/cmd/pinniped/cmd/get_kubeconfig.go +++ b/cmd/pinniped/cmd/get_kubeconfig.go @@ -6,20 +6,35 @@ 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" + apierrors "k8s.io/apimachinery/pkg/api/errors" + 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" + "github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1" + pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned" + "github.com/suzerain-io/pinniped/internal/controller/issuerconfig" "github.com/suzerain-io/pinniped/internal/here" ) const ( - getKubeConfigCmdTokenFlagName = "token" + getKubeConfigCmdTokenFlagName = "token" + getKubeConfigCmdKubeconfigFlagName = "kubeconfig" + getKubeConfigCmdKubeconfigContextFlagName = "kubeconfig-context" + getKubeConfigCmdPinnipedNamespaceFlagName = "pinniped-namespace" ) //nolint: gochecknoinits @@ -31,10 +46,15 @@ func init() { Short: "Print a kubeconfig for authenticating into a cluster via Pinniped", Long: here.Doc(` Print a kubeconfig for authenticating into a cluster via Pinniped. - - Assumes that you have admin-like access to the cluster using your - current kubeconfig context, in order to access Pinniped's metadata. - + + 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.: @@ -47,20 +67,54 @@ func init() { getKubeConfigCmd.Flags().StringP( getKubeConfigCmdTokenFlagName, - "t", "", - "The credential to include in the resulting kubeconfig output (Required)", + "", + "Credential to include in the resulting kubeconfig output (Required)", ) err := getKubeConfigCmd.MarkFlagRequired(getKubeConfigCmdTokenFlagName) if err != nil { panic(err) } + + getKubeConfigCmd.Flags().StringP( + getKubeConfigCmdKubeconfigFlagName, + "", + "", + "Path to the kubeconfig file", + ) + + getKubeConfigCmd.Flags().StringP( + getKubeConfigCmdKubeconfigContextFlagName, + "", + "", + "Kubeconfig context override", + ) + + getKubeConfigCmd.Flags().StringP( + getKubeConfigCmdPinnipedNamespaceFlagName, + "", + "pinniped", + "Namespace in which Pinniped was installed", + ) } func runGetKubeConfig(cmd *cobra.Command, _ []string) { token := cmd.Flag(getKubeConfigCmdTokenFlagName).Value.String() + kubeconfigPathOverride := cmd.Flag(getKubeConfigCmdKubeconfigFlagName).Value.String() + currentContextOverride := cmd.Flag(getKubeConfigCmdKubeconfigContextFlagName).Value.String() + pinnipedInstallationNamespace := cmd.Flag(getKubeConfigCmdPinnipedNamespaceFlagName).Value.String() - err := getKubeConfig(os.Stdout, token) + err := getKubeConfig( + os.Stdout, + os.Stderr, + token, + kubeconfigPathOverride, + currentContextOverride, + pinnipedInstallationNamespace, + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + return pinnipedclientset.NewForConfig(restConfig) + }, + ) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) @@ -68,45 +122,157 @@ func runGetKubeConfig(cmd *cobra.Command, _ []string) { } } -func getKubeConfig(outputWriter io.Writer, token string) error { - clusterName := "pinniped-cluster" - userName := "pinniped-user" +func getKubeConfig( + outputWriter io.Writer, + warningsWriter io.Writer, + token string, + kubeconfigPathOverride string, + currentContextNameOverride string, + pinnipedInstallationNamespace string, + kubeClientCreator func(restConfig *rest.Config) (pinnipedclientset.Interface, error), +) error { + if token == "" { + return fmt.Errorf("--" + getKubeConfigCmdTokenFlagName + " flag value cannot be empty") + } fullPathToSelf, err := os.Executable() if err != nil { return fmt.Errorf("could not find path to self: %w", err) } - config := v1.Config{ - Kind: "Config", - APIVersion: v1.SchemeGroupVersion.Version, - Preferences: v1.Preferences{ - Colors: false, // TODO what does this setting do? - Extensions: nil, - }, + clientConfig := newClientConfig(kubeconfigPathOverride, currentContextNameOverride) + + currentKubeConfig, err := clientConfig.RawConfig() + if err != nil { + return err + } + + credentialIssuerConfig, err := fetchPinnipedCredentialIssuerConfig(clientConfig, kubeClientCreator, pinnipedInstallationNamespace) + if err != nil { + return err + } + + v1Cluster, err := copyCurrentClusterFromExistingKubeConfig(err, currentKubeConfig, currentContextNameOverride) + if err != nil { + return err + } + + // TODO handle when credentialIssuerConfig has no Status or no KubeConfigInfo + + err = issueWarningForNonMatchingServerOrCA(v1Cluster, credentialIssuerConfig, warningsWriter) + if err != nil { + return err + } + + config := newPinnipedKubeconfig(v1Cluster, fullPathToSelf, token) + + err = writeConfigAsYAML(outputWriter, config) + if err != nil { + return err + } + + return nil +} + +func issueWarningForNonMatchingServerOrCA(v1Cluster v1.Cluster, credentialIssuerConfig *v1alpha1.CredentialIssuerConfig, warningsWriter io.Writer) error { + credentialIssuerConfigCA, err := base64.StdEncoding.DecodeString(credentialIssuerConfig.Status.KubeConfigInfo.CertificateAuthorityData) + if err != nil { + return err + } + if v1Cluster.Server != credentialIssuerConfig.Status.KubeConfigInfo.Server || + !bytes.Equal(v1Cluster.CertificateAuthorityData, credentialIssuerConfigCA) { + _, err := warningsWriter.Write([]byte("WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n")) + if err != nil { + return fmt.Errorf("output write error: %w", err) + } + } + return nil +} + +func fetchPinnipedCredentialIssuerConfig(clientConfig clientcmd.ClientConfig, kubeClientCreator func(restConfig *rest.Config) (pinnipedclientset.Interface, error), pinnipedInstallationNamespace string) (*v1alpha1.CredentialIssuerConfig, error) { + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + clientset, err := kubeClientCreator(restConfig) + if err != nil { + return nil, err + } + + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20) + defer cancelFunc() + + credentialIssuerConfig, err := clientset.CrdV1alpha1().CredentialIssuerConfigs(pinnipedInstallationNamespace).Get(ctx, issuerconfig.ConfigName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf( + `CredentialIssuerConfig "%s" was not found in namespace "%s". Is Pinniped installed on this cluster in namespace "%s"?`, + issuerconfig.ConfigName, + pinnipedInstallationNamespace, + pinnipedInstallationNamespace, + ) + } + return nil, err + } + + return credentialIssuerConfig, 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(err error, 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) 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: v1.Cluster{}, // TODO fill in server and cert authority and such - }, - }, - 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: ""}, // TODO fill in value - {Name: "PINNIPED_CA_BUNDLE", Value: ""}, // TODO fill in value - {Name: "PINNIPED_TOKEN", Value: token}, - }, - 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", - }, - }, + Cluster: v1Cluster, }, }, Contexts: []v1.NamedContext{ @@ -118,19 +284,25 @@ func getKubeConfig(outputWriter io.Writer, token string) error { }, }, }, + 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_TOKEN", Value: token}, + }, + 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, - Extensions: nil, } - - output, err := yaml.Marshal(&config) - if err != nil { - return fmt.Errorf("YAML serialization error: %w", err) - } - - _, err = fmt.Fprint(outputWriter, string(output)) - if err != nil { - return fmt.Errorf("output write error: %w", err) - } - - return nil } diff --git a/cmd/pinniped/cmd/get_kubeconfig_test.go b/cmd/pinniped/cmd/get_kubeconfig_test.go index feccb96bd..a99a47951 100644 --- a/cmd/pinniped/cmd/get_kubeconfig_test.go +++ b/cmd/pinniped/cmd/get_kubeconfig_test.go @@ -7,70 +7,422 @@ package cmd import ( "bytes" + "encoding/base64" + "fmt" "os" "testing" "github.com/sclevine/spec" "github.com/sclevine/spec/report" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/rest" + crdpinnipedv1alpha1 "github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1" + pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned" + pinnipedfake "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned/fake" "github.com/suzerain-io/pinniped/internal/here" ) +// TODO write a test for the help message and command line flags similar to server_test.go + +func expectedKubeconfigYAML(clusterCAData, clusterServer, command, token, pinnipedEndpoint, pinnipedCABundle 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_TOKEN + value: %s + installHint: |- + The Pinniped CLI is required to authenticate to the current cluster. + For more information, please visit https://pinniped.dev + `, clusterCAData, clusterServer, command, pinnipedEndpoint, pinnipedCABundle, token) +} + +func newCredentialIssuerConfig(server, certificateAuthorityData string) *crdpinnipedv1alpha1.CredentialIssuerConfig { + return &crdpinnipedv1alpha1.CredentialIssuerConfig{ + TypeMeta: metav1.TypeMeta{ + Kind: "CredentialIssuerConfig", + APIVersion: crdpinnipedv1alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "pinniped-config", + Namespace: "some-namespace", + }, + Status: crdpinnipedv1alpha1.CredentialIssuerConfigStatus{ + KubeConfigInfo: &crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo{ + Server: server, + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(certificateAuthorityData)), + }, + }, + } +} + func TestGetKubeConfig(t *testing.T) { spec.Run(t, "cmd.getKubeConfig", func(t *testing.T, when spec.G, it spec.S) { var r *require.Assertions - var buffer *bytes.Buffer + var outputBuffer *bytes.Buffer + var warningsBuffer *bytes.Buffer var fullPathToSelf string + var pinnipedClient *pinnipedfake.Clientset it.Before(func() { r = require.New(t) - buffer = new(bytes.Buffer) + + outputBuffer = new(bytes.Buffer) + warningsBuffer = new(bytes.Buffer) var err error fullPathToSelf, err = os.Executable() r.NoError(err) + + pinnipedClient = pinnipedfake.NewSimpleClientset() }) - it("writes the kubeconfig to the given writer", func() { - err := getKubeConfig(buffer, "some-token") - r.NoError(err) + when("the CredentialIssuerConfig is found on the cluster with a configuration that matches the existing kubeconfig", func() { + it.Before(func() { + r.NoError(pinnipedClient.Tracker().Add( + newCredentialIssuerConfig("https://fake-server-url-value", "fake-certificate-authority-data-value"), + )) + }) - expectedYAML := here.Docf(` - apiVersion: v1 - clusters: - - cluster: - server: "" - 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: "" - - name: PINNIPED_CA_BUNDLE - value: "" - - name: PINNIPED_TOKEN - value: some-token - installHint: |- - The Pinniped CLI is required to authenticate to the current cluster. - For more information, please visit https://pinniped.dev - `, fullPathToSelf) + it("writes the kubeconfig to the given writer", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://fake-server-url-value", restConfig.Host) + r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.NoError(err) + r.True(kubeClientCreatorFuncWasCalled) - r.Equal(expectedYAML, buffer.String()) + r.Empty(warningsBuffer.String()) + r.Equal(expectedKubeconfigYAML( + base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")), + "https://fake-server-url-value", + fullPathToSelf, + "some-token", + "https://fake-server-url-value", + "fake-certificate-authority-data-value", + ), outputBuffer.String()) + }) + + when("the currentContextOverride is used to specify a context other than the default context", func() { + it.Before(func() { + // update the Server and CertificateAuthorityData to make them match the other kubeconfig context + r.NoError(pinnipedClient.Tracker().Update( + schema.GroupVersionResource{ + Group: crdpinnipedv1alpha1.GroupName, + Version: crdpinnipedv1alpha1.SchemeGroupVersion.Version, + Resource: "credentialissuerconfigs", + }, + newCredentialIssuerConfig( + "https://some-other-fake-server-url-value", + "some-other-fake-certificate-authority-data-value", + ), + "some-namespace", + )) + }) + + when("that context exists", func() { + it("writes the kubeconfig to the given writer using the specified context", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "some-other-context", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://some-other-fake-server-url-value", restConfig.Host) + r.Equal("some-other-fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.NoError(err) + r.True(kubeClientCreatorFuncWasCalled) + + r.Empty(warningsBuffer.String()) + r.Equal(expectedKubeconfigYAML( + base64.StdEncoding.EncodeToString([]byte("some-other-fake-certificate-authority-data-value")), + "https://some-other-fake-server-url-value", + fullPathToSelf, + "some-token", + "https://some-other-fake-server-url-value", + "some-other-fake-certificate-authority-data-value", + ), outputBuffer.String()) + }) + }) + + when("that context does not exist the in the current kubeconfig", func() { + it("returns an error", func() { + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "this-context-name-does-not-exist-in-kubeconfig.yaml", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { return pinnipedClient, nil }, + ) + r.EqualError(err, `context "this-context-name-does-not-exist-in-kubeconfig.yaml" does not exist`) + r.Empty(warningsBuffer.String()) + r.Empty(outputBuffer.String()) + }) + }) + }) + + when("the token passed in is empty", func() { + it("returns an error", func() { + err := getKubeConfig(outputBuffer, + warningsBuffer, + "", + "./testdata/kubeconfig.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { return pinnipedClient, nil }, + ) + r.EqualError(err, "--token flag value cannot be empty") + r.Empty(warningsBuffer.String()) + r.Empty(outputBuffer.String()) + }) + }) + + when("the kubeconfig path passed refers to a file that does not exist", func() { + it("returns an error", func() { + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/this-file-does-not-exist.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { return pinnipedClient, nil }, + ) + r.EqualError(err, "stat ./testdata/this-file-does-not-exist.yaml: no such file or directory") + r.Empty(warningsBuffer.String()) + r.Empty(outputBuffer.String()) + }) + }) + + when("the kubeconfig path parameter is empty", func() { + it.Before(func() { + // Note that this is technically polluting other parallel tests in this file, but other tests + // are always specifying the kubeconfigPathOverride parameter, so they're not actually looking + // at the value of this environment variable. + r.NoError(os.Setenv("KUBECONFIG", "./testdata/kubeconfig.yaml")) + }) + + it.After(func() { + r.NoError(os.Unsetenv("KUBECONFIG")) + }) + + it("falls back to using the KUBECONFIG env var to find the kubeconfig file", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://fake-server-url-value", restConfig.Host) + r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.NoError(err) + r.True(kubeClientCreatorFuncWasCalled) + + r.Empty(warningsBuffer.String()) + r.Equal(expectedKubeconfigYAML( + base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")), + "https://fake-server-url-value", + fullPathToSelf, + "some-token", + "https://fake-server-url-value", + "fake-certificate-authority-data-value", + ), outputBuffer.String()) + }) + }) + + when("the wrong pinniped namespace is passed in", func() { + it("returns an error", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "", + "this-is-the-wrong-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://fake-server-url-value", restConfig.Host) + r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.EqualError(err, `CredentialIssuerConfig "pinniped-config" was not found in namespace "this-is-the-wrong-namespace". Is Pinniped installed on this cluster in namespace "this-is-the-wrong-namespace"?`) + r.True(kubeClientCreatorFuncWasCalled) + }) + }) }) + + when("the CredentialIssuerConfig is found on the cluster with a configuration that does not match the existing kubeconfig", func() { + when("the Server doesn't match", func() { + it.Before(func() { + r.NoError(pinnipedClient.Tracker().Add( + newCredentialIssuerConfig("non-matching-pinniped-server-url", "fake-certificate-authority-data-value"), + )) + }) + + it("writes the kubeconfig to the given writer using the values found in the local kubeconfig and issues a warning", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://fake-server-url-value", restConfig.Host) + r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.NoError(err) + r.True(kubeClientCreatorFuncWasCalled) + + r.Equal( + "WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n", + warningsBuffer.String(), + ) + r.Equal(expectedKubeconfigYAML( + base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")), + "https://fake-server-url-value", + fullPathToSelf, + "some-token", + "https://fake-server-url-value", + "fake-certificate-authority-data-value", + ), outputBuffer.String()) + }) + }) + + when("the CA doesn't match", func() { + it.Before(func() { + r.NoError(pinnipedClient.Tracker().Add( + newCredentialIssuerConfig("https://fake-server-url-value", "non-matching-certificate-authority-data-value"), + )) + }) + + it("writes the kubeconfig to the given writer using the values found in the local kubeconfig and issues a warning", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://fake-server-url-value", restConfig.Host) + r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.NoError(err) + r.True(kubeClientCreatorFuncWasCalled) + + r.Equal( + "WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n", + warningsBuffer.String(), + ) + r.Equal(expectedKubeconfigYAML( + base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")), + "https://fake-server-url-value", + fullPathToSelf, + "some-token", + "https://fake-server-url-value", + "fake-certificate-authority-data-value", + ), outputBuffer.String()) + }) + }) + }) + + when("the CredentialIssuerConfig does not exist on the cluster", func() { + it("returns an error", func() { + kubeClientCreatorFuncWasCalled := false + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + kubeClientCreatorFuncWasCalled = true + r.Equal("https://fake-server-url-value", restConfig.Host) + r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData)) + return pinnipedClient, nil + }, + ) + r.True(kubeClientCreatorFuncWasCalled) + r.EqualError(err, `CredentialIssuerConfig "pinniped-config" was not found in namespace "some-namespace". Is Pinniped installed on this cluster in namespace "some-namespace"?`) + r.Empty(warningsBuffer.String()) + r.Empty(outputBuffer.String()) + }) + }) + + when("there is an error while getting the CredentialIssuerConfig from the cluster", func() { + it("returns an error", func() { + err := getKubeConfig(outputBuffer, + warningsBuffer, + "some-token", + "./testdata/kubeconfig.yaml", + "", + "some-namespace", + func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { + return nil, fmt.Errorf("some error getting CredentialIssuerConfig") + }, + ) + r.EqualError(err, "some error getting CredentialIssuerConfig") + r.Empty(warningsBuffer.String()) + r.Empty(outputBuffer.String()) + }) + }) + }, spec.Parallel(), spec.Report(report.Terminal{})) } diff --git a/cmd/pinniped/cmd/testdata/kubeconfig.yaml b/cmd/pinniped/cmd/testdata/kubeconfig.yaml new file mode 100644 index 000000000..c89a226ec --- /dev/null +++ b/cmd/pinniped/cmd/testdata/kubeconfig.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +clusters: + - cluster: + certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== # fake-certificate-authority-data-value + server: https://fake-server-url-value + name: kind-kind + - cluster: + certificate-authority-data: c29tZS1vdGhlci1mYWtlLWNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhLXZhbHVl # some-other-fake-certificate-authority-data-value + server: https://some-other-fake-server-url-value + name: some-other-cluster +contexts: + - context: + cluster: kind-kind + user: kind-kind + name: kind-kind + - context: + cluster: some-other-cluster + user: some-other-user + name: some-other-context +current-context: kind-kind +kind: Config +preferences: {} +users: + - name: kind-kind + user: + client-certificate-data: ZmFrZS1jbGllbnQtY2VydGlmaWNhdGUtZGF0YS12YWx1ZQ== # fake-client-certificate-data-value + client-key-data: ZmFrZS1jbGllbnQta2V5LWRhdGEtdmFsdWU= # fake-client-key-data-value + - name: some-other-user + user: + client-certificate-data: c29tZS1vdGhlci1mYWtlLWNsaWVudC1jZXJ0aWZpY2F0ZS1kYXRhLXZhbHVl # some-other-fake-client-certificate-data-value + client-key-data: c29tZS1vdGhlci1mYWtlLWNsaWVudC1rZXktZGF0YS12YWx1ZQ== # some-other-fake-client-key-data-value diff --git a/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go b/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go index 071387891..99410e9bf 100644 --- a/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go +++ b/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go @@ -28,7 +28,7 @@ func CreateOrUpdateCredentialIssuerConfig( existingCredentialIssuerConfig, err := pinnipedClient. CrdV1alpha1(). CredentialIssuerConfigs(credentialIssuerConfigNamespace). - Get(ctx, configName, metav1.GetOptions{}) + Get(ctx, ConfigName, metav1.GetOptions{}) notFound := k8serrors.IsNotFound(err) if err != nil && !notFound { @@ -39,7 +39,7 @@ func CreateOrUpdateCredentialIssuerConfig( ctx, existingCredentialIssuerConfig, notFound, - configName, + ConfigName, credentialIssuerConfigNamespace, pinnipedClient, applyUpdatesToCredentialIssuerConfigFunc) diff --git a/internal/controller/issuerconfig/publisher.go b/internal/controller/issuerconfig/publisher.go index 46774514d..82bf52931 100644 --- a/internal/controller/issuerconfig/publisher.go +++ b/internal/controller/issuerconfig/publisher.go @@ -24,10 +24,10 @@ import ( const ( ClusterInfoNamespace = "kube-public" + ConfigName = "pinniped-config" + clusterInfoName = "cluster-info" clusterInfoConfigMapKey = "kubeconfig" - - configName = "pinniped-config" ) type publisherController struct { @@ -64,7 +64,7 @@ func NewPublisherController( ), withInformer( credentialIssuerConfigInformer, - pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configName, namespace), + pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(ConfigName, namespace), controllerlib.InformerOption{}, ), ) @@ -114,7 +114,7 @@ func (c *publisherController) Sync(ctx controllerlib.Context) error { existingCredentialIssuerConfigFromInformerCache, err := c.credentialIssuerConfigInformer. Lister(). CredentialIssuerConfigs(c.namespace). - Get(configName) + Get(ConfigName) notFound = k8serrors.IsNotFound(err) if err != nil && !notFound { return fmt.Errorf("could not get credentialissuerconfig: %w", err) @@ -131,7 +131,7 @@ func (c *publisherController) Sync(ctx controllerlib.Context) error { ctx.Context, existingCredentialIssuerConfigFromInformerCache, notFound, - configName, + ConfigName, c.namespace, c.pinnipedClient, updateServerAndCAFunc)