diff --git a/cmd/pinniped/cmd/kube_util.go b/cmd/pinniped/cmd/kube_util.go index 678824df3..3cfb4c9a9 100644 --- a/cmd/pinniped/cmd/kube_util.go +++ b/cmd/pinniped/cmd/kube_util.go @@ -1,34 +1,36 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd import ( + "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/internal/kubeclient" ) -// getConciergeClientsetFunc is a function that can return a clientset for the Concierge API given a +// getClientsetsFunc is a function that can return clients for the Concierge and Kubernetes APIs given a // clientConfig and the apiGroupSuffix with which the API is running. -type getConciergeClientsetFunc func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) +type getClientsetsFunc func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, kubernetes.Interface, aggregatorclient.Interface, error) -// getRealConciergeClientset returns a real implementation of a conciergeclientset.Interface. -func getRealConciergeClientset(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) { +// getRealClientsets returns real implementations of the Concierge and Kubernetes client interfaces. +func getRealClientsets(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, kubernetes.Interface, aggregatorclient.Interface, error) { restConfig, err := clientConfig.ClientConfig() if err != nil { - return nil, err + return nil, nil, nil, err } client, err := kubeclient.New( kubeclient.WithConfig(restConfig), kubeclient.WithMiddleware(groupsuffix.New(apiGroupSuffix)), ) if err != nil { - return nil, err + return nil, nil, nil, err } - return client.PinnipedConcierge, nil + return client.PinnipedConcierge, client.Kubernetes, client.Aggregation, nil } // newClientConfig returns a clientcmd.ClientConfig given an optional kubeconfig path override and diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 1386873d0..7a8fd8937 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd @@ -20,10 +20,12 @@ import ( coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" conciergeconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" @@ -38,7 +40,7 @@ import ( type kubeconfigDeps struct { getenv func(key string) string getPathToSelf func() (string, error) - getClientset getConciergeClientsetFunc + getClientsets getClientsetsFunc log plog.MinLogger } @@ -46,7 +48,7 @@ func kubeconfigRealDeps() kubeconfigDeps { return kubeconfigDeps{ getenv: os.Getenv, getPathToSelf: os.Executable, - getClientset: getRealConciergeClientset, + getClientsets: getRealClientsets, log: plog.New(), } } @@ -215,7 +217,7 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f return fmt.Errorf("could not load --kubeconfig/--kubeconfig-context: %w", err) } cluster := currentKubeConfig.Clusters[currentKubeconfigNames.ClusterName] - clientset, err := deps.getClientset(clientConfig, flags.concierge.apiGroupSuffix) + conciergeClient, kubeClient, aggregatorClient, err := deps.getClientsets(clientConfig, flags.concierge.apiGroupSuffix) if err != nil { return fmt.Errorf("could not configure Kubernetes client: %w", err) } @@ -228,13 +230,15 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f } if !flags.concierge.disabled { - credentialIssuer, err := waitForCredentialIssuer(ctx, clientset, flags, deps) + // Look up the Concierge's CredentialIssuer, and optionally wait for it to have no pending strategies showing in its status. + credentialIssuer, err := waitForCredentialIssuer(ctx, conciergeClient, flags, deps) if err != nil { return err } + // Decide which Concierge authenticator should be used in the resulting kubeconfig. authenticator, err := lookupAuthenticator( - clientset, + conciergeClient, flags.concierge.authenticatorType, flags.concierge.authenticatorName, deps.log, @@ -242,10 +246,15 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f if err != nil { return err } + + // Discover from the CredentialIssuer how the resulting kubeconfig should be configured to talk to this Concierge. if err := discoverConciergeParams(credentialIssuer, &flags, cluster, deps.log); err != nil { return err } - if err := discoverAuthenticatorParams(authenticator, &flags, deps.log); err != nil { + + // Discover how the resulting kubeconfig should interact with the selected authenticator. + // For a JWTAuthenticator, this includes discovering how to talk to the OIDC issuer configured in its spec fields. + if err := discoverAuthenticatorParams(ctx, authenticator, &flags, kubeClient, aggregatorClient, deps.log); err != nil { return err } @@ -255,6 +264,7 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f } if len(flags.oidc.issuer) > 0 { + // The OIDC provider may or may not be a Pinniped Supervisor. Find out. err = pinnipedSupervisorDiscovery(ctx, &flags, deps.log) if err != nil { return err @@ -488,7 +498,14 @@ func logStrategies(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer, l } } -func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconfigParams, log plog.MinLogger) error { +func discoverAuthenticatorParams( + ctx context.Context, + authenticator metav1.Object, + flags *getKubeconfigParams, + kubeClient kubernetes.Interface, + aggregatorClient aggregatorclient.Interface, + log plog.MinLogger, +) error { switch auth := authenticator.(type) { case *authenticationv1alpha1.WebhookAuthenticator: // If the --concierge-authenticator-type/--concierge-authenticator-name flags were not set explicitly, set @@ -520,19 +537,130 @@ func discoverAuthenticatorParams(authenticator metav1.Object, flags *getKubeconf } // If the --oidc-ca-bundle flags was not set explicitly, default it to the - // spec.tls.certificateAuthorityData field of the JWTAuthenticator. - if len(flags.oidc.caBundle) == 0 && auth.Spec.TLS != nil && auth.Spec.TLS.CertificateAuthorityData != "" { - decoded, err := base64.StdEncoding.DecodeString(auth.Spec.TLS.CertificateAuthorityData) + // spec.tls.certificateAuthorityData field of the JWTAuthenticator, if that field is set, or else + // try to discover it from the spec.tls.certificateAuthorityDataSource, if that field is set. + if len(flags.oidc.caBundle) == 0 && auth.Spec.TLS != nil { + err := discoverOIDCCABundle(ctx, auth, flags, kubeClient, aggregatorClient, log) if err != nil { - return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", auth.Name, err) + return err } - log.Info("discovered OIDC CA bundle", "roots", countCACerts(decoded)) - flags.oidc.caBundle = decoded } } return nil } +func discoverOIDCCABundle( + ctx context.Context, + jwtAuthenticator *authenticationv1alpha1.JWTAuthenticator, + flags *getKubeconfigParams, + kubeClient kubernetes.Interface, + aggregatorClient aggregatorclient.Interface, + log plog.MinLogger, +) error { + if jwtAuthenticator.Spec.TLS.CertificateAuthorityData != "" { + decodedCABundleData, err := base64.StdEncoding.DecodeString(jwtAuthenticator.Spec.TLS.CertificateAuthorityData) + if err != nil { + return fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s has invalid spec.tls.certificateAuthorityData: %w", jwtAuthenticator.Name, err) + } + log.Info("discovered OIDC CA bundle", "roots", countCACerts(decodedCABundleData)) + flags.oidc.caBundle = decodedCABundleData + } else if jwtAuthenticator.Spec.TLS.CertificateAuthorityDataSource != nil { + caBundleData, err := discoverOIDCCABundleFromCertificateAuthorityDataSource( + ctx, jwtAuthenticator, flags.concierge.apiGroupSuffix, kubeClient, aggregatorClient, log) + if err != nil { + return err + } + flags.oidc.caBundle = caBundleData + } + return nil +} + +func discoverOIDCCABundleFromCertificateAuthorityDataSource( + ctx context.Context, + jwtAuthenticator *authenticationv1alpha1.JWTAuthenticator, + apiGroupSuffix string, + kubeClient kubernetes.Interface, + aggregatorClient aggregatorclient.Interface, + log plog.MinLogger, +) ([]byte, error) { + conciergeNamespace, err := discoverConciergeNamespace(ctx, apiGroupSuffix, aggregatorClient) + if err != nil { + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but encountered error discovering namespace of Concierge for JWTAuthenticator %s: %w", jwtAuthenticator.Name, err) + } + log.Info("discovered Concierge namespace for API group suffix", "apiGroupSuffix", apiGroupSuffix) + + var caBundleData []byte + var keyExisted bool + caSource := jwtAuthenticator.Spec.TLS.CertificateAuthorityDataSource + + // Note that the Kind, Name, and Key fields must all be non-empty, and Kind must be Secret or ConfigMap, due to CRD validations. + switch caSource.Kind { + case authenticationv1alpha1.CertificateAuthorityDataSourceKindConfigMap: + caBundleConfigMap, err := kubeClient.CoreV1().ConfigMaps(conciergeNamespace).Get(ctx, caSource.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but encountered error getting %s %s/%s specified by JWTAuthenticator %s spec.tls.certificateAuthorityDataSource: %w", + caSource.Kind, conciergeNamespace, caSource.Name, jwtAuthenticator.Name, err) + } + var caBundleDataStr string + caBundleDataStr, keyExisted = caBundleConfigMap.Data[caSource.Key] + caBundleData = []byte(caBundleDataStr) + case authenticationv1alpha1.CertificateAuthorityDataSourceKindSecret: + caBundleSecret, err := kubeClient.CoreV1().Secrets(conciergeNamespace).Get(ctx, caSource.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but encountered error getting %s %s/%s specified by JWTAuthenticator %s spec.tls.certificateAuthorityDataSource: %w", + caSource.Kind, conciergeNamespace, caSource.Name, jwtAuthenticator.Name, err) + } + caBundleData, keyExisted = caBundleSecret.Data[caSource.Key] + default: + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator %s spec.tls.certificateAuthorityDataSource.Kind value %q is not supported by this CLI version", + jwtAuthenticator.Name, caSource.Kind) + } + + if !keyExisted { + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but key %q specified by JWTAuthenticator %s spec.tls.certificateAuthorityDataSource.key does not exist in %s %s/%s", + caSource.Key, jwtAuthenticator.Name, caSource.Kind, conciergeNamespace, caSource.Name) + } + + if len(caBundleData) == 0 { + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but key %q specified by JWTAuthenticator %s spec.tls.certificateAuthorityDataSource.key exists but has empty value in %s %s/%s", + caSource.Key, jwtAuthenticator.Name, caSource.Kind, conciergeNamespace, caSource.Name) + } + + numCACerts := countCACerts(caBundleData) + if numCACerts == 0 { + return nil, fmt.Errorf("tried to autodiscover --oidc-ca-bundle, but value at key %q specified by JWTAuthenticator %s spec.tls.certificateAuthorityDataSource.key does not contain any CA certificates in %s %s/%s", + caSource.Key, jwtAuthenticator.Name, caSource.Kind, conciergeNamespace, caSource.Name) + } + + log.Info("discovered OIDC CA bundle from JWTAuthenticator spec.tls.certificateAuthorityDataSource", "roots", numCACerts) + return caBundleData, nil +} + +func discoverConciergeNamespace(ctx context.Context, apiGroupSuffix string, aggregatorClient aggregatorclient.Interface) (string, error) { + // Let's look for the APIService for the API group of the Concierge's TokenCredentialRequest aggregated API. + apiGroup := "login.concierge." + apiGroupSuffix + + // List all APIServices. + apiServiceList, err := aggregatorClient.ApiregistrationV1().APIServices().List(ctx, metav1.ListOptions{}) + if err != nil { + return "", fmt.Errorf("error listing APIServices: %w", err) + } + + // Find one with the expected API group name. + for _, apiService := range apiServiceList.Items { + if apiService.Spec.Group == apiGroup { + if apiService.Spec.Service.Namespace != "" { + // We are assuming that all API versions (e.g. v1alpha1) of this API group are backed by service(s) + // in the same namespace, which is the namespace of the Concierge hosting this API suffix. + return apiService.Spec.Service.Namespace, nil + } + } + } + + // Couldn't find any APIService for the expected API group name which contained a namespace reference in its spec. + return "", fmt.Errorf("could not find APIService with non-empty spec.service.namespace for API group %s", apiGroup) +} + func getConciergeFrontend(credentialIssuer *conciergeconfigv1alpha1.CredentialIssuer, mode conciergeModeFlag) (*conciergeconfigv1alpha1.CredentialIssuerFrontend, error) { for _, strategy := range credentialIssuer.Status.Strategies { // Skip unhealthy strategies. diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 55d29373c..51a671bf4 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd @@ -15,10 +15,16 @@ import ( "time" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" kubetesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/clientcmd" + v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" + aggregatorfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" "k8s.io/utils/ptr" authenticationv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" @@ -64,14 +70,69 @@ func TestGetKubeconfig(t *testing.T) { } } - jwtAuthenticator := func(issuerCABundle string, issuerURL string) runtime.Object { + caBundleInSecret := func(issuerCABundle, secretName, secretNamespace, secretDataKey string) runtime.Object { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + }, + Data: map[string][]byte{ + secretDataKey: []byte(issuerCABundle), + "other": []byte("unrelated"), + }, + } + } + + caBundleInConfigmap := func(issuerCABundle, cmName, cmNamespace, cmDataKey string) runtime.Object { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: cmName, + Namespace: cmNamespace, + }, + Data: map[string]string{ + cmDataKey: issuerCABundle, + "other": "unrelated", + }, + } + } + + jwtAuthenticator := func(issuerCABundle, issuerURL string) *authenticationv1alpha1.JWTAuthenticator { + encodedCABundle := "" + if issuerCABundle != "" { + encodedCABundle = base64.StdEncoding.EncodeToString([]byte(issuerCABundle)) + } return &authenticationv1alpha1.JWTAuthenticator{ ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, Spec: authenticationv1alpha1.JWTAuthenticatorSpec{ Issuer: issuerURL, Audience: "test-audience", TLS: &authenticationv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(issuerCABundle)), + CertificateAuthorityData: encodedCABundle, + }, + }, + } + } + + jwtAuthenticatorWithCABundleDataSource := func(sourceKind, sourceName, sourceKey, issuerURL string) runtime.Object { + authenticator := jwtAuthenticator("", issuerURL) + authenticator.Spec.TLS.CertificateAuthorityDataSource = &authenticationv1alpha1.CertificateAuthorityDataSourceSpec{ + Kind: authenticationv1alpha1.CertificateAuthorityDataSourceKind(sourceKind), + Name: sourceName, + Key: sourceKey, + } + return authenticator + } + + apiService := func(group, version, serviceNamespace string) *v1.APIService { + return &v1.APIService{ + ObjectMeta: metav1.ObjectMeta{ + Name: version + "." + group, + }, + Spec: v1.APIServiceSpec{ + Group: group, + Version: version, + Service: &v1.ServiceReference{ + Namespace: serviceNamespace, }, }, } @@ -144,7 +205,11 @@ func TestGetKubeconfig(t *testing.T) { getPathToSelfErr error getClientsetErr error conciergeObjects func(string, string) []runtime.Object + kubeObjects func(string) []runtime.Object + apiServiceObjects []runtime.Object conciergeReactions []kubetesting.Reactor + kubeReactions []kubetesting.Reactor + apiServiceReactions []kubetesting.Reactor oidcDiscoveryResponse func(string) string oidcDiscoveryStatusCode int idpsDiscoveryResponse string @@ -656,6 +721,321 @@ func TestGetKubeconfig(t *testing.T) { return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7` + "\n") }, }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but Secret not found", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but encountered error getting Secret test-concierge-namespace/my-ca-secret specified by JWTAuthenticator test-authenticator spec.tls.certificateAuthorityDataSource: secrets "my-ca-secret" not found` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in ConfigMap, but ConfigMap not found", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("ConfigMap", "my-ca-configmap", "ca.crt", issuerURL), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but encountered error getting ConfigMap test-concierge-namespace/my-ca-configmap specified by JWTAuthenticator test-authenticator spec.tls.certificateAuthorityDataSource: configmaps "my-ca-configmap" not found` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but invalid TLS bundle found in Secret", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + kubeObjects: func(issuerCABundle string) []runtime.Object { + return []runtime.Object{ + caBundleInSecret("invalid CA bundle data", "my-ca-secret", "test-concierge-namespace", "ca.crt"), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but value at key "ca.crt" specified by JWTAuthenticator test-authenticator spec.tls.certificateAuthorityDataSource.key does not contain any CA certificates in Secret test-concierge-namespace/my-ca-secret` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but specified key not found in Secret", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + kubeObjects: func(issuerCABundle string) []runtime.Object { + return []runtime.Object{ + caBundleInSecret(issuerCABundle, "my-ca-secret", "test-concierge-namespace", "wrong_key_name"), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but key "ca.crt" specified by JWTAuthenticator test-authenticator spec.tls.certificateAuthorityDataSource.key does not exist in Secret test-concierge-namespace/my-ca-secret` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but specified key has empty value in Secret", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + kubeObjects: func(issuerCABundle string) []runtime.Object { + return []runtime.Object{ + caBundleInSecret("", "my-ca-secret", "test-concierge-namespace", "ca.crt"), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but key "ca.crt" specified by JWTAuthenticator test-authenticator spec.tls.certificateAuthorityDataSource.key exists but has empty value in Secret test-concierge-namespace/my-ca-secret` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle source, but source's Kind is not supported", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Unsupported-Value", "my-ca-secret", "ca.crt", issuerURL), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator spec.tls.certificateAuthorityDataSource.Kind value "Unsupported-Value" is not supported by this CLI version` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but no related APIService found", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("unrelated.example.com", "v1alpha1", "test-concierge-namespace"), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but encountered error discovering namespace of Concierge for JWTAuthenticator test-authenticator: could not find APIService with non-empty spec.service.namespace for API group login.concierge.pinniped.dev` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but related APIService has empty namespace in spec", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", ""), + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but encountered error discovering namespace of Concierge for JWTAuthenticator test-authenticator: could not find APIService with non-empty spec.service.namespace for API group login.concierge.pinniped.dev` + "\n") + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in Secret, but error when listing APIServices", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + apiServiceReactions: []kubetesting.Reactor{ + &kubetesting.SimpleReactor{ + Verb: "*", + Resource: "apiservices", + Reaction: func(kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("some list error") + }, + }, + }, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) testutil.RequireErrorStringFunc { + return testutil.WantExactErrorString(`Error: tried to autodiscover --oidc-ca-bundle, but encountered error discovering namespace of Concierge for JWTAuthenticator test-authenticator: error listing APIServices: some list error` + "\n") + }, + }, { name: "autodetect JWT authenticator, invalid substring in audience", args: func(issuerCABundle string, issuerURL string) []string { @@ -1600,6 +1980,257 @@ func TestGetKubeconfig(t *testing.T) { base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, }, + { + name: "autodetect JWT authenticator with CA bundle in Secret", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("Secret", "my-ca-secret", "ca.crt", issuerURL), + } + }, + kubeObjects: func(issuerCABundle string) []runtime.Object { + return []runtime.Object{ + caBundleInSecret(issuerCABundle, "my-ca-secret", "test-concierge-namespace", "ca.crt"), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + apiService("unrelated.pinniped.dev", "v1alpha1", "unrelated-namespace"), + apiService("login.concierge.pinniped.dev", "v1alpha2", "test-concierge-namespace"), + }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC CA bundle from JWTAuthenticator spec.tls.certificateAuthorityDataSource {"roots": 1}`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + server: https://fake-server-url-value + name: kind-cluster-pinniped + contexts: + - context: + cluster: kind-cluster-pinniped + user: kind-user-pinniped + name: kind-context-pinniped + current-context: kind-context-pinniped + kind: Config + preferences: {} + users: + - name: kind-user-pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience,username,groups + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli + for more details + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in ConfigMap", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("ConfigMap", "my-ca-configmap", "ca.crt", issuerURL), + } + }, + kubeObjects: func(issuerCABundle string) []runtime.Object { + return []runtime.Object{ + caBundleInConfigmap(issuerCABundle, "my-ca-configmap", "test-concierge-namespace", "ca.crt"), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.pinniped.dev", "v1alpha1", "test-concierge-namespace"), + apiService("unrelated.pinniped.dev", "v1alpha1", "unrelated-namespace"), + apiService("login.concierge.pinniped.dev", "v1alpha2", "test-concierge-namespace"), + }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "pinniped.dev"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC CA bundle from JWTAuthenticator spec.tls.certificateAuthorityDataSource {"roots": 1}`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + server: https://fake-server-url-value + name: kind-cluster-pinniped + contexts: + - context: + cluster: kind-cluster-pinniped + user: kind-user-pinniped + name: kind-context-pinniped + current-context: kind-context-pinniped + kind: Config + preferences: {} + users: + - name: kind-user-pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience,username,groups + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli + for more details + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "autodetect JWT authenticator with CA bundle in ConfigMap with a custom API group suffix", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-api-group-suffix=acme.com", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticatorWithCABundleDataSource("ConfigMap", "my-ca-configmap", "ca.crt", issuerURL), + } + }, + kubeObjects: func(issuerCABundle string) []runtime.Object { + return []runtime.Object{ + caBundleInConfigmap(issuerCABundle, "my-ca-configmap", "test-concierge-namespace", "ca.crt"), + } + }, + apiServiceObjects: []runtime.Object{ + apiService("login.concierge.acme.com", "v1alpha1", "test-concierge-namespace"), + apiService("unrelated.pinniped.dev", "v1alpha1", "unrelated-namespace"), + apiService("login.concierge.pinniped.dev", "v1alpha2", "another-unrelated-namespace"), + }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered CredentialIssuer {"name": "test-credential-issuer"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge operating in TokenCredentialRequest API mode`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge endpoint {"endpoint": "https://fake-server-url-value"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge certificate authority bundle {"roots": 0}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered JWTAuthenticator {"name": "test-authenticator"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC issuer {"issuer": "` + issuerURL + `"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC audience {"audience": "test-audience"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered Concierge namespace for API group suffix {"apiGroupSuffix": "acme.com"}`, + `2099-08-08T13:57:36.123456Z info cmd/kubeconfig.go: discovered OIDC CA bundle from JWTAuthenticator spec.tls.certificateAuthorityDataSource {"roots": 1}`, + } + }, + wantAPIGroupSuffix: "acme.com", + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + server: https://fake-server-url-value + name: kind-cluster-pinniped + contexts: + - context: + cluster: kind-cluster-pinniped + user: kind-user-pinniped + name: kind-context-pinniped + current-context: kind-context-pinniped + kind: Config + preferences: {} + users: + - name: kind-user-pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=acme.com + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience,username,groups + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + installHint: The pinniped CLI does not appear to be installed. See https://get.pinniped.dev/cli + for more details + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, { name: "autodetect nothing, set a bunch of options", args: func(issuerCABundle string, issuerURL string) []string { @@ -3211,6 +3842,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var issuerEndpointPtr *string @@ -3245,6 +3877,37 @@ func TestGetKubeconfig(t *testing.T) { }), nil) issuerEndpointPtr = ptr.To(testServer.URL) + getClientsetFunc := func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, kubernetes.Interface, aggregatorclient.Interface, error) { + if tt.wantAPIGroupSuffix == "" { + require.Equal(t, "pinniped.dev", apiGroupSuffix) // "pinniped.dev" = api group suffix default + } else { + require.Equal(t, tt.wantAPIGroupSuffix, apiGroupSuffix) + } + if tt.getClientsetErr != nil { + return nil, nil, nil, tt.getClientsetErr + } + fakeAggregatorClient := aggregatorfake.NewSimpleClientset(tt.apiServiceObjects...) + fakeKubeClient := fake.NewClientset() + if tt.kubeObjects != nil { + kubeObjects := tt.kubeObjects(string(testServerCA)) + fakeKubeClient = fake.NewClientset(kubeObjects...) + } + fakeConciergeClient := conciergefake.NewSimpleClientset() + if tt.conciergeObjects != nil { + fakeConciergeClient = conciergefake.NewSimpleClientset(tt.conciergeObjects(string(testServerCA), testServer.URL)...) + } + if len(tt.conciergeReactions) > 0 { + fakeConciergeClient.ReactionChain = slices.Concat(tt.conciergeReactions, fakeConciergeClient.ReactionChain) + } + if len(tt.kubeReactions) > 0 { + fakeKubeClient.ReactionChain = slices.Concat(tt.kubeReactions, fakeKubeClient.ReactionChain) + } + if len(tt.apiServiceReactions) > 0 { + fakeAggregatorClient.ReactionChain = slices.Concat(tt.apiServiceReactions, fakeAggregatorClient.ReactionChain) + } + return fakeConciergeClient, fakeKubeClient, fakeAggregatorClient, nil + } + var log bytes.Buffer cmd := kubeconfigCommand(kubeconfigDeps{ @@ -3257,25 +3920,8 @@ func TestGetKubeconfig(t *testing.T) { } return ".../path/to/pinniped", nil }, - getClientset: func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) { - if tt.wantAPIGroupSuffix == "" { - require.Equal(t, "pinniped.dev", apiGroupSuffix) // "pinniped.dev" = api group suffix default - } else { - require.Equal(t, tt.wantAPIGroupSuffix, apiGroupSuffix) - } - if tt.getClientsetErr != nil { - return nil, tt.getClientsetErr - } - fake := conciergefake.NewSimpleClientset() - if tt.conciergeObjects != nil { - fake = conciergefake.NewSimpleClientset(tt.conciergeObjects(string(testServerCA), testServer.URL)...) - } - if len(tt.conciergeReactions) > 0 { - fake.ReactionChain = slices.Concat(tt.conciergeReactions, fake.ReactionChain) - } - return fake, nil - }, - log: plog.TestConsoleLogger(t, &log), + getClientsets: getClientsetFunc, + log: plog.TestConsoleLogger(t, &log), }) require.NotNil(t, cmd) diff --git a/cmd/pinniped/cmd/whoami.go b/cmd/pinniped/cmd/whoami.go index c6c30403f..82d60cd0a 100644 --- a/cmd/pinniped/cmd/whoami.go +++ b/cmd/pinniped/cmd/whoami.go @@ -1,4 +1,4 @@ -// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2021-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd @@ -25,14 +25,14 @@ import ( ) type whoamiDeps struct { - getenv func(key string) string - getClientset getConciergeClientsetFunc + getenv func(key string) string + getClientsets getClientsetsFunc } func whoamiRealDeps() whoamiDeps { return whoamiDeps{ - getenv: os.Getenv, - getClientset: getRealConciergeClientset, + getenv: os.Getenv, + getClientsets: getRealClientsets, } } @@ -82,7 +82,7 @@ func newWhoamiCommand(deps whoamiDeps) *cobra.Command { func runWhoami(output io.Writer, deps whoamiDeps, flags *whoamiFlags) error { clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride) - clientset, err := deps.getClientset(clientConfig, flags.apiGroupSuffix) + conciergeClient, _, _, err := deps.getClientsets(clientConfig, flags.apiGroupSuffix) if err != nil { return fmt.Errorf("could not configure Kubernetes client: %w", err) } @@ -108,7 +108,7 @@ func runWhoami(output io.Writer, deps whoamiDeps, flags *whoamiFlags) error { defer cancelFunc() } - whoAmI, err := clientset.IdentityV1alpha1().WhoAmIRequests().Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{}) + whoAmI, err := conciergeClient.IdentityV1alpha1().WhoAmIRequests().Create(ctx, &identityv1alpha1.WhoAmIRequest{}, metav1.CreateOptions{}) if err != nil { hint := "" if apierrors.IsNotFound(err) { diff --git a/cmd/pinniped/cmd/whoami_test.go b/cmd/pinniped/cmd/whoami_test.go index c01ef99fb..c38af8528 100644 --- a/cmd/pinniped/cmd/whoami_test.go +++ b/cmd/pinniped/cmd/whoami_test.go @@ -1,4 +1,4 @@ -// Copyright 2023-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2023-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cmd @@ -11,8 +11,10 @@ import ( "github.com/stretchr/testify/require" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" kubetesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/clientcmd" + aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1" conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" @@ -290,14 +292,15 @@ func TestWhoami(t *testing.T) { wantStderr: "Error: could not complete WhoAmIRequest (is the Pinniped WhoAmI API running and healthy?): whoamirequests.identity.concierge.pinniped.dev \"whatever\" not found\n", }, } + for _, test := range tests { t.Run(test.name, func(t *testing.T) { - getClientset := func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, error) { + getClientsetFunc := func(clientConfig clientcmd.ClientConfig, apiGroupSuffix string) (conciergeclientset.Interface, kubernetes.Interface, aggregatorclient.Interface, error) { if test.gettingClientsetErr != nil { - return nil, test.gettingClientsetErr + return nil, nil, nil, test.gettingClientsetErr } - clientset := conciergefake.NewSimpleClientset() - clientset.PrependReactor("create", "whoamirequests", func(_ kubetesting.Action) (bool, runtime.Object, error) { + conciergeClient := conciergefake.NewSimpleClientset() + conciergeClient.PrependReactor("create", "whoamirequests", func(_ kubetesting.Action) (bool, runtime.Object, error) { if test.callingAPIErr != nil { return true, nil, test.callingAPIErr } @@ -316,13 +319,14 @@ func TestWhoami(t *testing.T) { }, }, nil }) - return clientset, nil + return conciergeClient, nil, nil, nil } + cmd := newWhoamiCommand(whoamiDeps{ getenv: func(key string) string { return test.env[key] }, - getClientset: getClientset, + getClientsets: getClientsetFunc, }) stdout, stderr := bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}) diff --git a/hack/install-linter.sh b/hack/install-linter.sh index e0671fb7e..ba30e81aa 100755 --- a/hack/install-linter.sh +++ b/hack/install-linter.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2022-2024 the Pinniped contributors. All Rights Reserved. +# Copyright 2022-2025 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail @@ -13,7 +13,11 @@ go version lint_version="v$(cat hack/lib/lint-version.txt)" -echo "Installing golangci-lint@${lint_version}" +# Find the toolchain version from our go.mod file. "go install" pays attention to $GOTOOLCHAIN. +GOTOOLCHAIN=$(sed -rn 's/^toolchain (go[0-9\.]+)$/\1/p' go.mod) +export GOTOOLCHAIN + +echo "Installing golangci-lint@${lint_version} using toolchain ${GOTOOLCHAIN}" # Install the same version of the linter that the pipelines will use # so you can get the same results when running the linter locally. diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 2d54abcd3..eed238f08 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -1,4 +1,4 @@ -// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package integration @@ -174,7 +174,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -276,7 +275,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, "--oidc-scopes", "offline_access,openid,pinniped:request-audience", // does not request username or groups @@ -379,7 +377,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", "--oidc-skip-listen", - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -517,7 +514,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", "--oidc-skip-listen", - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -651,7 +647,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--oidc-skip-browser", "--oidc-skip-listen", "--upstream-identity-provider-flow", "cli_password", // create a kubeconfig configured to use the cli_password flow - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -731,7 +726,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--upstream-identity-provider-name", oidcIdentityProvider.Name, "--upstream-identity-provider-type", "oidc", "--upstream-identity-provider-flow", "cli_password", - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes @@ -1118,7 +1112,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", federationDomainCABundlePath, "--upstream-identity-provider-flow", "browser_authcode", "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, @@ -1174,7 +1167,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", federationDomainCABundlePath, "--upstream-identity-provider-flow", "browser_authcode", "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, @@ -1230,7 +1222,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", federationDomainCABundlePath, "--upstream-identity-provider-flow", "cli_password", // put cli_password in the kubeconfig, so we can override it with the env var "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, @@ -1319,7 +1310,6 @@ func TestE2EFullIntegration_Browser(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, "--oidc-skip-browser", - "--oidc-ca-bundle", federationDomainCABundlePath, "--oidc-session-cache", sessionCachePath, "--credential-cache", credentialCachePath, // use default for --oidc-scopes, which is to request all relevant scopes