CLI get-kubeconfig command reads kubeconfig and CredentialIssuerConfig

This commit is contained in:
Ryan Richard 2020-09-14 19:07:18 -07:00
parent 872330bee9
commit 4379d2772c
5 changed files with 652 additions and 97 deletions

View File

@ -6,20 +6,35 @@ SPDX-License-Identifier: Apache-2.0
package cmd package cmd
import ( import (
"bytes"
"context"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"os" "os"
"time"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/spf13/cobra" "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" 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" 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" "github.com/suzerain-io/pinniped/internal/here"
) )
const ( const (
getKubeConfigCmdTokenFlagName = "token" getKubeConfigCmdTokenFlagName = "token"
getKubeConfigCmdKubeconfigFlagName = "kubeconfig"
getKubeConfigCmdKubeconfigContextFlagName = "kubeconfig-context"
getKubeConfigCmdPinnipedNamespaceFlagName = "pinniped-namespace"
) )
//nolint: gochecknoinits //nolint: gochecknoinits
@ -32,8 +47,13 @@ func init() {
Long: here.Doc(` Long: here.Doc(`
Print a kubeconfig for authenticating into a cluster via Pinniped. Print a kubeconfig for authenticating into a cluster via Pinniped.
Assumes that you have admin-like access to the cluster using your Requires admin-like access to the cluster using the current
current kubeconfig context, in order to access Pinniped's metadata. 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 Prints a kubeconfig which is suitable to access the cluster using
Pinniped as the authentication mechanism. This kubeconfig output Pinniped as the authentication mechanism. This kubeconfig output
@ -47,20 +67,54 @@ func init() {
getKubeConfigCmd.Flags().StringP( getKubeConfigCmd.Flags().StringP(
getKubeConfigCmdTokenFlagName, 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) err := getKubeConfigCmd.MarkFlagRequired(getKubeConfigCmdTokenFlagName)
if err != nil { if err != nil {
panic(err) 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) { func runGetKubeConfig(cmd *cobra.Command, _ []string) {
token := cmd.Flag(getKubeConfigCmdTokenFlagName).Value.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 { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %s\n", err.Error()) _, _ = 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 { func getKubeConfig(
clusterName := "pinniped-cluster" outputWriter io.Writer,
userName := "pinniped-user" 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() fullPathToSelf, err := os.Executable()
if err != nil { if err != nil {
return fmt.Errorf("could not find path to self: %w", err) return fmt.Errorf("could not find path to self: %w", err)
} }
config := v1.Config{ 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", Kind: "Config",
APIVersion: v1.SchemeGroupVersion.Version, APIVersion: v1.SchemeGroupVersion.Version,
Preferences: v1.Preferences{ Preferences: v1.Preferences{},
Colors: false, // TODO what does this setting do?
Extensions: nil,
},
Clusters: []v1.NamedCluster{ Clusters: []v1.NamedCluster{
{ {
Name: clusterName, Name: clusterName,
Cluster: v1.Cluster{}, // TODO fill in server and cert authority and such Cluster: v1Cluster,
},
},
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",
},
},
}, },
}, },
Contexts: []v1.NamedContext{ 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, 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
} }

View File

@ -7,40 +7,33 @@ package cmd
import ( import (
"bytes" "bytes"
"encoding/base64"
"fmt"
"os" "os"
"testing" "testing"
"github.com/sclevine/spec" "github.com/sclevine/spec"
"github.com/sclevine/spec/report" "github.com/sclevine/spec/report"
"github.com/stretchr/testify/require" "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" "github.com/suzerain-io/pinniped/internal/here"
) )
func TestGetKubeConfig(t *testing.T) { // TODO write a test for the help message and command line flags similar to server_test.go
spec.Run(t, "cmd.getKubeConfig", func(t *testing.T, when spec.G, it spec.S) {
var r *require.Assertions
var buffer *bytes.Buffer
var fullPathToSelf string
it.Before(func() { func expectedKubeconfigYAML(clusterCAData, clusterServer, command, token, pinnipedEndpoint, pinnipedCABundle string) string {
r = require.New(t) return here.Docf(`
buffer = new(bytes.Buffer)
var err error
fullPathToSelf, err = os.Executable()
r.NoError(err)
})
it("writes the kubeconfig to the given writer", func() {
err := getKubeConfig(buffer, "some-token")
r.NoError(err)
expectedYAML := here.Docf(`
apiVersion: v1 apiVersion: v1
clusters: clusters:
- cluster: - cluster:
server: "" certificate-authority-data: %s
server: %s
name: pinniped-cluster name: pinniped-cluster
contexts: contexts:
- context: - context:
@ -60,17 +53,376 @@ func TestGetKubeConfig(t *testing.T) {
command: %s command: %s
env: env:
- name: PINNIPED_K8S_API_ENDPOINT - name: PINNIPED_K8S_API_ENDPOINT
value: "" value: %s
- name: PINNIPED_CA_BUNDLE - name: PINNIPED_CA_BUNDLE
value: "" value: %s
- name: PINNIPED_TOKEN - name: PINNIPED_TOKEN
value: some-token value: %s
installHint: |- installHint: |-
The Pinniped CLI is required to authenticate to the current cluster. The Pinniped CLI is required to authenticate to the current cluster.
For more information, please visit https://pinniped.dev For more information, please visit https://pinniped.dev
`, fullPathToSelf) `, clusterCAData, clusterServer, command, pinnipedEndpoint, pinnipedCABundle, token)
}
r.Equal(expectedYAML, buffer.String()) 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 outputBuffer *bytes.Buffer
var warningsBuffer *bytes.Buffer
var fullPathToSelf string
var pinnipedClient *pinnipedfake.Clientset
it.Before(func() {
r = require.New(t)
outputBuffer = new(bytes.Buffer)
warningsBuffer = new(bytes.Buffer)
var err error
fullPathToSelf, err = os.Executable()
r.NoError(err)
pinnipedClient = pinnipedfake.NewSimpleClientset()
}) })
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"),
))
})
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.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{})) }, spec.Parallel(), spec.Report(report.Terminal{}))
} }

View File

@ -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

View File

@ -28,7 +28,7 @@ func CreateOrUpdateCredentialIssuerConfig(
existingCredentialIssuerConfig, err := pinnipedClient. existingCredentialIssuerConfig, err := pinnipedClient.
CrdV1alpha1(). CrdV1alpha1().
CredentialIssuerConfigs(credentialIssuerConfigNamespace). CredentialIssuerConfigs(credentialIssuerConfigNamespace).
Get(ctx, configName, metav1.GetOptions{}) Get(ctx, ConfigName, metav1.GetOptions{})
notFound := k8serrors.IsNotFound(err) notFound := k8serrors.IsNotFound(err)
if err != nil && !notFound { if err != nil && !notFound {
@ -39,7 +39,7 @@ func CreateOrUpdateCredentialIssuerConfig(
ctx, ctx,
existingCredentialIssuerConfig, existingCredentialIssuerConfig,
notFound, notFound,
configName, ConfigName,
credentialIssuerConfigNamespace, credentialIssuerConfigNamespace,
pinnipedClient, pinnipedClient,
applyUpdatesToCredentialIssuerConfigFunc) applyUpdatesToCredentialIssuerConfigFunc)

View File

@ -24,10 +24,10 @@ import (
const ( const (
ClusterInfoNamespace = "kube-public" ClusterInfoNamespace = "kube-public"
ConfigName = "pinniped-config"
clusterInfoName = "cluster-info" clusterInfoName = "cluster-info"
clusterInfoConfigMapKey = "kubeconfig" clusterInfoConfigMapKey = "kubeconfig"
configName = "pinniped-config"
) )
type publisherController struct { type publisherController struct {
@ -64,7 +64,7 @@ func NewPublisherController(
), ),
withInformer( withInformer(
credentialIssuerConfigInformer, credentialIssuerConfigInformer,
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configName, namespace), pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(ConfigName, namespace),
controllerlib.InformerOption{}, controllerlib.InformerOption{},
), ),
) )
@ -114,7 +114,7 @@ func (c *publisherController) Sync(ctx controllerlib.Context) error {
existingCredentialIssuerConfigFromInformerCache, err := c.credentialIssuerConfigInformer. existingCredentialIssuerConfigFromInformerCache, err := c.credentialIssuerConfigInformer.
Lister(). Lister().
CredentialIssuerConfigs(c.namespace). CredentialIssuerConfigs(c.namespace).
Get(configName) Get(ConfigName)
notFound = k8serrors.IsNotFound(err) notFound = k8serrors.IsNotFound(err)
if err != nil && !notFound { if err != nil && !notFound {
return fmt.Errorf("could not get credentialissuerconfig: %w", err) return fmt.Errorf("could not get credentialissuerconfig: %w", err)
@ -131,7 +131,7 @@ func (c *publisherController) Sync(ctx controllerlib.Context) error {
ctx.Context, ctx.Context,
existingCredentialIssuerConfigFromInformerCache, existingCredentialIssuerConfigFromInformerCache,
notFound, notFound,
configName, ConfigName,
c.namespace, c.namespace,
c.pinnipedClient, c.pinnipedClient,
updateServerAndCAFunc) updateServerAndCAFunc)