2020-09-16 14:19:51 +00:00
// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
2020-09-12 00:56:05 +00:00
package cmd
import (
2020-09-15 02:07:18 +00:00
"bytes"
"context"
"encoding/base64"
2020-09-12 00:56:05 +00:00
"fmt"
"io"
"os"
2020-09-15 02:07:18 +00:00
"time"
2020-09-12 00:56:05 +00:00
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
2020-09-15 02:07:18 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2020-09-12 00:56:05 +00:00
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
2020-09-15 02:07:18 +00:00
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
2020-09-12 00:56:05 +00:00
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
2020-10-30 20:09:14 +00:00
configv1alpha1 "go.pinniped.dev/generated/1.19/apis/concierge/config/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/1.19/client/concierge/clientset/versioned"
2020-09-18 19:56:24 +00:00
"go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/here"
2020-11-10 16:23:42 +00:00
"go.pinniped.dev/internal/plog"
2020-09-12 00:56:05 +00:00
)
//nolint: gochecknoinits
func init ( ) {
2020-09-21 20:42:54 +00:00
rootCmd . AddCommand ( newGetKubeConfigCommand ( ) . Command ( ) )
}
type getKubeConfigFlags struct {
2020-10-30 19:02:21 +00:00
token string
kubeconfig string
contextOverride string
namespace string
authenticatorName string
authenticatorType string
2020-09-15 14:04:25 +00:00
}
type getKubeConfigCommand struct {
2020-09-21 20:42:54 +00:00
flags getKubeConfigFlags
// Test mocking points
getPathToSelf func ( ) ( string , error )
kubeClientCreator func ( restConfig * rest . Config ) ( pinnipedclientset . Interface , error )
2020-09-15 14:04:25 +00:00
}
2020-09-21 20:42:54 +00:00
func newGetKubeConfigCommand ( ) * getKubeConfigCommand {
return & getKubeConfigCommand {
flags : getKubeConfigFlags {
2020-11-05 06:29:43 +00:00
namespace : "pinniped-concierge" ,
2020-09-21 20:42:54 +00:00
} ,
getPathToSelf : os . Executable ,
kubeClientCreator : func ( restConfig * rest . Config ) ( pinnipedclientset . Interface , error ) {
return pinnipedclientset . NewForConfig ( restConfig )
} ,
2020-09-15 14:04:25 +00:00
}
2020-09-21 20:42:54 +00:00
}
2020-09-15 14:04:25 +00:00
2020-09-21 20:42:54 +00:00
func ( c * getKubeConfigCommand ) Command ( ) * cobra . Command {
cmd := & cobra . Command {
RunE : c . run ,
2020-09-12 00:56:05 +00:00
Args : cobra . NoArgs , // do not accept positional arguments for this command
Use : "get-kubeconfig" ,
Short : "Print a kubeconfig for authenticating into a cluster via Pinniped" ,
Long : here . Doc ( `
Print a kubeconfig for authenticating into a cluster via Pinniped .
2020-09-15 02:07:18 +00:00
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 .
2020-09-12 00:56:05 +00:00
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 . :
pinniped get - kubeconfig -- token $ MY_TOKEN > $ HOME / mycluster - kubeconfig
kubectl -- kubeconfig $ HOME / mycluster - kubeconfig get pods
` ) ,
}
2020-09-21 20:42:54 +00:00
cmd . Flags ( ) . StringVar ( & c . flags . token , "token" , "" , "Credential to include in the resulting kubeconfig output (Required)" )
cmd . Flags ( ) . StringVar ( & c . flags . kubeconfig , "kubeconfig" , c . flags . kubeconfig , "Path to the kubeconfig file" )
cmd . Flags ( ) . StringVar ( & c . flags . contextOverride , "kubeconfig-context" , c . flags . contextOverride , "Kubeconfig context override" )
cmd . Flags ( ) . StringVar ( & c . flags . namespace , "pinniped-namespace" , c . flags . namespace , "Namespace in which Pinniped was installed" )
2020-12-08 01:40:20 +00:00
cmd . Flags ( ) . StringVar ( & c . flags . authenticatorType , "authenticator-type" , c . flags . authenticatorType , "Authenticator type (e.g., 'webhook', 'jwt')" )
2020-10-30 19:02:21 +00:00
cmd . Flags ( ) . StringVar ( & c . flags . authenticatorName , "authenticator-name" , c . flags . authenticatorType , "Authenticator name" )
2020-10-06 00:09:51 +00:00
mustMarkRequired ( cmd , "token" )
2020-11-10 16:23:42 +00:00
plog . RemoveKlogGlobalFlags ( )
2020-09-21 20:42:54 +00:00
return cmd
2020-09-15 14:04:25 +00:00
}
2020-09-12 00:56:05 +00:00
2020-09-21 20:42:54 +00:00
func ( c * getKubeConfigCommand ) run ( cmd * cobra . Command , args [ ] string ) error {
fullPathToSelf , err := c . getPathToSelf ( )
2020-09-12 00:56:05 +00:00
if err != nil {
2020-09-21 20:42:54 +00:00
return fmt . Errorf ( "could not find path to self: %w" , err )
2020-09-12 00:56:05 +00:00
}
2020-09-21 20:42:54 +00:00
clientConfig := newClientConfig ( c . flags . kubeconfig , c . flags . contextOverride )
2020-09-12 00:56:05 +00:00
2020-09-21 20:42:54 +00:00
currentKubeConfig , err := clientConfig . RawConfig ( )
2020-09-12 00:56:05 +00:00
if err != nil {
2020-09-21 20:42:54 +00:00
return err
2020-09-12 00:56:05 +00:00
}
2020-09-21 20:42:54 +00:00
restConfig , err := clientConfig . ClientConfig ( )
if err != nil {
return err
}
clientset , err := c . kubeClientCreator ( restConfig )
2020-09-15 02:07:18 +00:00
if err != nil {
return err
}
2020-10-30 19:02:21 +00:00
authenticatorType , authenticatorName := c . flags . authenticatorType , c . flags . authenticatorName
if authenticatorType == "" || authenticatorName == "" {
authenticatorType , authenticatorName , err = getDefaultAuthenticator ( clientset , c . flags . namespace )
2020-09-21 22:41:30 +00:00
if err != nil {
return err
}
}
2020-11-02 21:39:43 +00:00
credentialIssuer , err := fetchPinnipedCredentialIssuer ( clientset , c . flags . namespace )
2020-09-15 02:07:18 +00:00
if err != nil {
return err
}
2020-11-02 21:39:43 +00:00
if credentialIssuer . Status . KubeConfigInfo == nil {
return constable . Error ( ` CredentialIssuer "pinniped-config" was missing KubeConfigInfo ` )
2020-09-15 14:04:25 +00:00
}
2020-09-21 20:42:54 +00:00
v1Cluster , err := copyCurrentClusterFromExistingKubeConfig ( currentKubeConfig , c . flags . contextOverride )
2020-09-15 02:07:18 +00:00
if err != nil {
return err
}
2020-11-02 21:39:43 +00:00
err = issueWarningForNonMatchingServerOrCA ( v1Cluster , credentialIssuer , cmd . ErrOrStderr ( ) )
2020-09-15 02:07:18 +00:00
if err != nil {
return err
}
2020-10-30 19:02:21 +00:00
config := newPinnipedKubeconfig ( v1Cluster , fullPathToSelf , c . flags . token , c . flags . namespace , authenticatorType , authenticatorName )
2020-09-15 02:07:18 +00:00
2020-09-21 20:42:54 +00:00
err = writeConfigAsYAML ( cmd . OutOrStdout ( ) , config )
2020-09-15 02:07:18 +00:00
if err != nil {
return err
}
return nil
}
2020-11-02 21:39:43 +00:00
func issueWarningForNonMatchingServerOrCA ( v1Cluster v1 . Cluster , credentialIssuer * configv1alpha1 . CredentialIssuer , warningsWriter io . Writer ) error {
credentialIssuerCA , err := base64 . StdEncoding . DecodeString ( credentialIssuer . Status . KubeConfigInfo . CertificateAuthorityData )
2020-09-15 02:07:18 +00:00
if err != nil {
return err
}
2020-11-02 21:39:43 +00:00
if v1Cluster . Server != credentialIssuer . Status . KubeConfigInfo . Server ||
! bytes . Equal ( v1Cluster . CertificateAuthorityData , credentialIssuerCA ) {
_ , err := warningsWriter . Write ( [ ] byte ( "WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuer on the cluster. Using local kubeconfig values.\n" ) )
2020-09-15 02:07:18 +00:00
if err != nil {
return fmt . Errorf ( "output write error: %w" , err )
}
}
return nil
}
2020-10-30 19:02:21 +00:00
type noAuthenticatorError struct { Namespace string }
2020-09-21 22:41:30 +00:00
2020-10-30 19:02:21 +00:00
func ( e noAuthenticatorError ) Error ( ) string {
return fmt . Sprintf ( ` no authenticators were found in namespace %q ` , e . Namespace )
2020-09-21 22:41:30 +00:00
}
2020-10-30 19:02:21 +00:00
type indeterminateAuthenticatorError struct { Namespace string }
2020-09-21 22:41:30 +00:00
2020-10-30 19:02:21 +00:00
func ( e indeterminateAuthenticatorError ) Error ( ) string {
2020-09-21 22:41:30 +00:00
return fmt . Sprintf (
2020-10-30 19:02:21 +00:00
` multiple authenticators were found in namespace %q, so --authenticator-name/--authenticator-type must be specified ` ,
2020-09-21 22:41:30 +00:00
e . Namespace ,
)
}
2020-10-30 19:02:21 +00:00
func getDefaultAuthenticator ( clientset pinnipedclientset . Interface , namespace string ) ( string , string , error ) {
2020-09-21 22:41:30 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Second * 20 )
defer cancelFunc ( )
2020-10-30 16:39:26 +00:00
webhooks , err := clientset . AuthenticationV1alpha1 ( ) . WebhookAuthenticators ( namespace ) . List ( ctx , metav1 . ListOptions { } )
2020-09-21 22:41:30 +00:00
if err != nil {
return "" , "" , err
}
2020-10-30 19:02:21 +00:00
type ref struct { authenticatorType , authenticatorName string }
authenticators := make ( [ ] ref , 0 , len ( webhooks . Items ) )
2020-09-21 22:41:30 +00:00
for _ , webhook := range webhooks . Items {
2020-10-30 19:02:21 +00:00
authenticators = append ( authenticators , ref { authenticatorType : "webhook" , authenticatorName : webhook . Name } )
2020-09-21 22:41:30 +00:00
}
2020-10-30 19:02:21 +00:00
if len ( authenticators ) == 0 {
return "" , "" , noAuthenticatorError { namespace }
2020-09-21 22:41:30 +00:00
}
2020-10-30 19:02:21 +00:00
if len ( authenticators ) > 1 {
return "" , "" , indeterminateAuthenticatorError { namespace }
2020-09-21 22:41:30 +00:00
}
2020-10-30 19:02:21 +00:00
return authenticators [ 0 ] . authenticatorType , authenticators [ 0 ] . authenticatorName , nil
2020-09-21 22:41:30 +00:00
}
2020-11-02 21:39:43 +00:00
func fetchPinnipedCredentialIssuer ( clientset pinnipedclientset . Interface , pinnipedInstallationNamespace string ) ( * configv1alpha1 . CredentialIssuer , error ) {
2020-09-15 02:07:18 +00:00
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Second * 20 )
defer cancelFunc ( )
2020-11-02 21:39:43 +00:00
credentialIssuers , err := clientset . ConfigV1alpha1 ( ) . CredentialIssuers ( pinnipedInstallationNamespace ) . List ( ctx , metav1 . ListOptions { } )
2020-09-15 02:07:18 +00:00
if err != nil {
return nil , err
}
2020-11-02 21:39:43 +00:00
if len ( credentialIssuers . Items ) == 0 {
Rename many of resources that are created in Kubernetes by Pinniped
New resource naming conventions:
- Do not repeat the Kind in the name,
e.g. do not call it foo-cluster-role-binding, just call it foo
- Names will generally start with a prefix to identify our component,
so when a user lists all objects of that kind, they can tell to which
component it is related,
e.g. `kubectl get configmaps` would list one named "pinniped-config"
- It should be possible for an operator to make the word "pinniped"
mostly disappear if they choose, by specifying the app_name in
values.yaml, to the extent that is practical (but not from APIService
names because those are hardcoded in golang)
- Each role/clusterrole and its corresponding binding have the same name
- Pinniped resource names that must be known by the server golang code
are passed to the code at run time via ConfigMap, rather than
hardcoded in the golang code. This also allows them to be prepended
with the app_name from values.yaml while creating the ConfigMap.
- Since the CLI `get-kubeconfig` command cannot guess the name of the
CredentialIssuerConfig resource in advance anymore, it lists all
CredentialIssuerConfig in the app's namespace and returns an error
if there is not exactly one found, and then uses that one regardless
of its name
2020-09-18 22:56:50 +00:00
return nil , constable . Error ( fmt . Sprintf (
2020-11-02 21:39:43 +00:00
` No CredentialIssuer was found in namespace "%s". Is Pinniped installed on this cluster in namespace "%s"? ` ,
Rename many of resources that are created in Kubernetes by Pinniped
New resource naming conventions:
- Do not repeat the Kind in the name,
e.g. do not call it foo-cluster-role-binding, just call it foo
- Names will generally start with a prefix to identify our component,
so when a user lists all objects of that kind, they can tell to which
component it is related,
e.g. `kubectl get configmaps` would list one named "pinniped-config"
- It should be possible for an operator to make the word "pinniped"
mostly disappear if they choose, by specifying the app_name in
values.yaml, to the extent that is practical (but not from APIService
names because those are hardcoded in golang)
- Each role/clusterrole and its corresponding binding have the same name
- Pinniped resource names that must be known by the server golang code
are passed to the code at run time via ConfigMap, rather than
hardcoded in the golang code. This also allows them to be prepended
with the app_name from values.yaml while creating the ConfigMap.
- Since the CLI `get-kubeconfig` command cannot guess the name of the
CredentialIssuerConfig resource in advance anymore, it lists all
CredentialIssuerConfig in the app's namespace and returns an error
if there is not exactly one found, and then uses that one regardless
of its name
2020-09-18 22:56:50 +00:00
pinnipedInstallationNamespace ,
pinnipedInstallationNamespace ,
) )
}
2020-11-02 21:39:43 +00:00
if len ( credentialIssuers . Items ) > 1 {
Rename many of resources that are created in Kubernetes by Pinniped
New resource naming conventions:
- Do not repeat the Kind in the name,
e.g. do not call it foo-cluster-role-binding, just call it foo
- Names will generally start with a prefix to identify our component,
so when a user lists all objects of that kind, they can tell to which
component it is related,
e.g. `kubectl get configmaps` would list one named "pinniped-config"
- It should be possible for an operator to make the word "pinniped"
mostly disappear if they choose, by specifying the app_name in
values.yaml, to the extent that is practical (but not from APIService
names because those are hardcoded in golang)
- Each role/clusterrole and its corresponding binding have the same name
- Pinniped resource names that must be known by the server golang code
are passed to the code at run time via ConfigMap, rather than
hardcoded in the golang code. This also allows them to be prepended
with the app_name from values.yaml while creating the ConfigMap.
- Since the CLI `get-kubeconfig` command cannot guess the name of the
CredentialIssuerConfig resource in advance anymore, it lists all
CredentialIssuerConfig in the app's namespace and returns an error
if there is not exactly one found, and then uses that one regardless
of its name
2020-09-18 22:56:50 +00:00
return nil , constable . Error ( fmt . Sprintf (
2020-11-02 21:39:43 +00:00
` More than one CredentialIssuer was found in namespace "%s" ` ,
Rename many of resources that are created in Kubernetes by Pinniped
New resource naming conventions:
- Do not repeat the Kind in the name,
e.g. do not call it foo-cluster-role-binding, just call it foo
- Names will generally start with a prefix to identify our component,
so when a user lists all objects of that kind, they can tell to which
component it is related,
e.g. `kubectl get configmaps` would list one named "pinniped-config"
- It should be possible for an operator to make the word "pinniped"
mostly disappear if they choose, by specifying the app_name in
values.yaml, to the extent that is practical (but not from APIService
names because those are hardcoded in golang)
- Each role/clusterrole and its corresponding binding have the same name
- Pinniped resource names that must be known by the server golang code
are passed to the code at run time via ConfigMap, rather than
hardcoded in the golang code. This also allows them to be prepended
with the app_name from values.yaml while creating the ConfigMap.
- Since the CLI `get-kubeconfig` command cannot guess the name of the
CredentialIssuerConfig resource in advance anymore, it lists all
CredentialIssuerConfig in the app's namespace and returns an error
if there is not exactly one found, and then uses that one regardless
of its name
2020-09-18 22:56:50 +00:00
pinnipedInstallationNamespace ,
) )
}
2020-11-02 21:39:43 +00:00
return & credentialIssuers . Items [ 0 ] , nil
2020-09-15 02:07:18 +00:00
}
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
}
2020-09-15 15:00:00 +00:00
func copyCurrentClusterFromExistingKubeConfig ( currentKubeConfig clientcmdapi . Config , currentContextNameOverride string ) ( v1 . Cluster , error ) {
2020-09-15 02:07:18 +00:00
v1Cluster := v1 . Cluster { }
contextName := currentKubeConfig . CurrentContext
if currentContextNameOverride != "" {
contextName = currentContextNameOverride
}
2020-09-15 15:00:00 +00:00
err := v1 . Convert_api_Cluster_To_v1_Cluster (
2020-09-15 02:07:18 +00:00
currentKubeConfig . Clusters [ currentKubeConfig . Contexts [ contextName ] . Cluster ] ,
& v1Cluster ,
nil ,
)
if err != nil {
return v1 . Cluster { } , err
}
return v1Cluster , nil
}
2020-10-30 19:02:21 +00:00
func newPinnipedKubeconfig ( v1Cluster v1 . Cluster , fullPathToSelf string , token string , namespace string , authenticatorType string , authenticatorName string ) v1 . Config {
2020-09-15 02:07:18 +00:00
clusterName := "pinniped-cluster"
userName := "pinniped-user"
return v1 . Config {
Kind : "Config" ,
APIVersion : v1 . SchemeGroupVersion . Version ,
Preferences : v1 . Preferences { } ,
2020-09-12 00:56:05 +00:00
Clusters : [ ] v1 . NamedCluster {
{
Name : clusterName ,
2020-09-15 02:07:18 +00:00
Cluster : v1Cluster ,
} ,
} ,
Contexts : [ ] v1 . NamedContext {
{
Name : clusterName ,
Context : v1 . Context {
Cluster : clusterName ,
AuthInfo : userName ,
} ,
2020-09-12 00:56:05 +00:00
} ,
} ,
AuthInfos : [ ] v1 . NamedAuthInfo {
{
Name : userName ,
AuthInfo : v1 . AuthInfo {
Exec : & v1 . ExecConfig {
Command : fullPathToSelf ,
Args : [ ] string { "exchange-credential" } ,
Env : [ ] v1 . ExecEnvVar {
2020-09-17 23:05:56 +00:00
{
Name : "PINNIPED_K8S_API_ENDPOINT" ,
Value : v1Cluster . Server ,
} ,
{
Name : "PINNIPED_CA_BUNDLE" ,
Value : string ( v1Cluster . CertificateAuthorityData ) } ,
{
Name : "PINNIPED_NAMESPACE" ,
Value : namespace ,
} ,
{
Name : "PINNIPED_TOKEN" ,
Value : token ,
} ,
2020-09-21 22:41:30 +00:00
{
2020-10-30 19:02:21 +00:00
Name : "PINNIPED_AUTHENTICATOR_TYPE" ,
Value : authenticatorType ,
2020-09-21 22:41:30 +00:00
} ,
{
2020-10-30 19:02:21 +00:00
Name : "PINNIPED_AUTHENTICATOR_NAME" ,
Value : authenticatorName ,
2020-09-21 22:41:30 +00:00
} ,
2020-09-12 00:56:05 +00:00
} ,
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 ,
}
}