2020-09-12 00:56:05 +00:00
/ *
Copyright 2020 VMware , Inc .
SPDX - License - Identifier : Apache - 2.0
* /
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
apierrors "k8s.io/apimachinery/pkg/api/errors"
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-09-15 02:07:18 +00:00
"github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1"
pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned"
2020-09-15 14:04:25 +00:00
"github.com/suzerain-io/pinniped/internal/constable"
2020-09-15 02:07:18 +00:00
"github.com/suzerain-io/pinniped/internal/controller/issuerconfig"
2020-09-12 00:56:05 +00:00
"github.com/suzerain-io/pinniped/internal/here"
)
const (
2020-09-15 02:07:18 +00:00
getKubeConfigCmdTokenFlagName = "token"
getKubeConfigCmdKubeconfigFlagName = "kubeconfig"
getKubeConfigCmdKubeconfigContextFlagName = "kubeconfig-context"
getKubeConfigCmdPinnipedNamespaceFlagName = "pinniped-namespace"
2020-09-12 00:56:05 +00:00
)
//nolint: gochecknoinits
func init ( ) {
2020-09-15 14:04:25 +00:00
rootCmd . AddCommand ( newGetKubeConfigCmd ( os . Args , os . Stdout , os . Stderr ) . cmd )
}
type getKubeConfigCommand struct {
// runFunc is called by the cobra.Command.Run hook. It is included here for
// testability.
runFunc func (
stdout , stderr io . Writer ,
token , kubeconfigPathOverride , currentContextOverride , pinnipedInstallationNamespace string ,
)
// cmd is the cobra.Command for this CLI command. It is included here for
// testability.
cmd * cobra . Command
}
func newGetKubeConfigCmd ( args [ ] string , stdout , stderr io . Writer ) * getKubeConfigCommand {
c := & getKubeConfigCommand {
runFunc : runGetKubeConfig ,
}
c . cmd = & cobra . Command {
Run : func ( cmd * cobra . Command , _ [ ] string ) {
token := cmd . Flag ( getKubeConfigCmdTokenFlagName ) . Value . String ( )
kubeconfigPathOverride := cmd . Flag ( getKubeConfigCmdKubeconfigFlagName ) . Value . String ( )
currentContextOverride := cmd . Flag ( getKubeConfigCmdKubeconfigContextFlagName ) . Value . String ( )
pinnipedInstallationNamespace := cmd . Flag ( getKubeConfigCmdPinnipedNamespaceFlagName ) . Value . String ( )
c . runFunc (
stdout ,
stderr ,
token ,
kubeconfigPathOverride ,
currentContextOverride ,
pinnipedInstallationNamespace ,
)
} ,
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-15 14:04:25 +00:00
c . cmd . SetArgs ( args )
c . cmd . SetOut ( stdout )
c . cmd . SetErr ( stderr )
2020-09-12 00:56:05 +00:00
2020-09-15 14:04:25 +00:00
c . cmd . Flags ( ) . StringP (
2020-09-12 00:56:05 +00:00
getKubeConfigCmdTokenFlagName ,
"" ,
2020-09-15 02:07:18 +00:00
"" ,
"Credential to include in the resulting kubeconfig output (Required)" ,
2020-09-12 00:56:05 +00:00
)
2020-09-15 14:04:25 +00:00
err := c . cmd . MarkFlagRequired ( getKubeConfigCmdTokenFlagName )
2020-09-12 00:56:05 +00:00
if err != nil {
panic ( err )
}
2020-09-15 02:07:18 +00:00
2020-09-15 14:04:25 +00:00
c . cmd . Flags ( ) . StringP (
2020-09-15 02:07:18 +00:00
getKubeConfigCmdKubeconfigFlagName ,
"" ,
"" ,
"Path to the kubeconfig file" ,
)
2020-09-15 14:04:25 +00:00
c . cmd . Flags ( ) . StringP (
2020-09-15 02:07:18 +00:00
getKubeConfigCmdKubeconfigContextFlagName ,
"" ,
"" ,
"Kubeconfig context override" ,
)
2020-09-15 14:04:25 +00:00
c . cmd . Flags ( ) . StringP (
2020-09-15 02:07:18 +00:00
getKubeConfigCmdPinnipedNamespaceFlagName ,
"" ,
"pinniped" ,
"Namespace in which Pinniped was installed" ,
)
2020-09-12 00:56:05 +00:00
2020-09-15 14:04:25 +00:00
return c
}
2020-09-12 00:56:05 +00:00
2020-09-15 14:04:25 +00:00
func runGetKubeConfig (
stdout , stderr io . Writer ,
token , kubeconfigPathOverride , currentContextOverride , pinnipedInstallationNamespace string ,
) {
2020-09-15 02:07:18 +00:00
err := getKubeConfig (
2020-09-15 14:04:25 +00:00
stdout ,
stderr ,
2020-09-15 02:07:18 +00:00
token ,
kubeconfigPathOverride ,
currentContextOverride ,
pinnipedInstallationNamespace ,
func ( restConfig * rest . Config ) ( pinnipedclientset . Interface , error ) {
return pinnipedclientset . NewForConfig ( restConfig )
} ,
)
2020-09-12 00:56:05 +00:00
if err != nil {
_ , _ = fmt . Fprintf ( os . Stderr , "error: %s\n" , err . Error ( ) )
os . Exit ( 1 )
}
}
2020-09-15 02:07:18 +00:00
func getKubeConfig (
outputWriter io . Writer ,
warningsWriter io . Writer ,
token string ,
kubeconfigPathOverride string ,
currentContextNameOverride string ,
pinnipedInstallationNamespace string ,
kubeClientCreator func ( restConfig * rest . Config ) ( pinnipedclientset . Interface , error ) ,
) error {
if token == "" {
return fmt . Errorf ( "--" + getKubeConfigCmdTokenFlagName + " flag value cannot be empty" )
}
2020-09-12 00:56:05 +00:00
fullPathToSelf , err := os . Executable ( )
if err != nil {
return fmt . Errorf ( "could not find path to self: %w" , err )
}
2020-09-15 02:07:18 +00:00
clientConfig := newClientConfig ( kubeconfigPathOverride , currentContextNameOverride )
currentKubeConfig , err := clientConfig . RawConfig ( )
if err != nil {
return err
}
credentialIssuerConfig , err := fetchPinnipedCredentialIssuerConfig ( clientConfig , kubeClientCreator , pinnipedInstallationNamespace )
if err != nil {
return err
}
2020-09-15 14:04:25 +00:00
if credentialIssuerConfig . Status . KubeConfigInfo == nil {
return constable . Error ( ` CredentialIssuerConfig "pinniped-config" was missing KubeConfigInfo ` )
}
2020-09-15 02:07:18 +00:00
v1Cluster , err := copyCurrentClusterFromExistingKubeConfig ( err , currentKubeConfig , currentContextNameOverride )
if err != nil {
return err
}
err = issueWarningForNonMatchingServerOrCA ( v1Cluster , credentialIssuerConfig , warningsWriter )
if err != nil {
return err
}
config := newPinnipedKubeconfig ( v1Cluster , fullPathToSelf , token )
err = writeConfigAsYAML ( outputWriter , config )
if err != nil {
return err
}
return nil
}
func issueWarningForNonMatchingServerOrCA ( v1Cluster v1 . Cluster , credentialIssuerConfig * v1alpha1 . CredentialIssuerConfig , warningsWriter io . Writer ) error {
credentialIssuerConfigCA , err := base64 . StdEncoding . DecodeString ( credentialIssuerConfig . Status . KubeConfigInfo . CertificateAuthorityData )
if err != nil {
return err
}
if v1Cluster . Server != credentialIssuerConfig . Status . KubeConfigInfo . Server ||
! bytes . Equal ( v1Cluster . CertificateAuthorityData , credentialIssuerConfigCA ) {
_ , err := warningsWriter . Write ( [ ] byte ( "WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n" ) )
if err != nil {
return fmt . Errorf ( "output write error: %w" , err )
}
}
return nil
}
func fetchPinnipedCredentialIssuerConfig ( clientConfig clientcmd . ClientConfig , kubeClientCreator func ( restConfig * rest . Config ) ( pinnipedclientset . Interface , error ) , pinnipedInstallationNamespace string ) ( * v1alpha1 . CredentialIssuerConfig , error ) {
restConfig , err := clientConfig . ClientConfig ( )
if err != nil {
return nil , err
}
clientset , err := kubeClientCreator ( restConfig )
if err != nil {
return nil , err
}
ctx , cancelFunc := context . WithTimeout ( context . Background ( ) , time . Second * 20 )
defer cancelFunc ( )
credentialIssuerConfig , err := clientset . CrdV1alpha1 ( ) . CredentialIssuerConfigs ( pinnipedInstallationNamespace ) . Get ( ctx , issuerconfig . ConfigName , metav1 . GetOptions { } )
if err != nil {
if apierrors . IsNotFound ( err ) {
return nil , fmt . Errorf (
` CredentialIssuerConfig "%s" was not found in namespace "%s". Is Pinniped installed on this cluster in namespace "%s"? ` ,
issuerconfig . ConfigName ,
pinnipedInstallationNamespace ,
pinnipedInstallationNamespace ,
)
}
return nil , err
}
return credentialIssuerConfig , nil
}
func newClientConfig ( kubeconfigPathOverride string , currentContextName string ) clientcmd . ClientConfig {
loadingRules := clientcmd . NewDefaultClientConfigLoadingRules ( )
loadingRules . ExplicitPath = kubeconfigPathOverride
clientConfig := clientcmd . NewNonInteractiveDeferredLoadingClientConfig ( loadingRules , & clientcmd . ConfigOverrides {
CurrentContext : currentContextName ,
} )
return clientConfig
}
func writeConfigAsYAML ( outputWriter io . Writer , config v1 . Config ) error {
output , err := yaml . Marshal ( & config )
if err != nil {
return fmt . Errorf ( "YAML serialization error: %w" , err )
}
_ , err = outputWriter . Write ( output )
if err != nil {
return fmt . Errorf ( "output write error: %w" , err )
}
return nil
}
func copyCurrentClusterFromExistingKubeConfig ( err error , currentKubeConfig clientcmdapi . Config , currentContextNameOverride string ) ( v1 . Cluster , error ) {
v1Cluster := v1 . Cluster { }
contextName := currentKubeConfig . CurrentContext
if currentContextNameOverride != "" {
contextName = currentContextNameOverride
}
err = v1 . Convert_api_Cluster_To_v1_Cluster (
currentKubeConfig . Clusters [ currentKubeConfig . Contexts [ contextName ] . Cluster ] ,
& v1Cluster ,
nil ,
)
if err != nil {
return v1 . Cluster { } , err
}
return v1Cluster , nil
}
func newPinnipedKubeconfig ( v1Cluster v1 . Cluster , fullPathToSelf string , token string ) v1 . Config {
clusterName := "pinniped-cluster"
userName := "pinniped-user"
return v1 . Config {
Kind : "Config" ,
APIVersion : v1 . SchemeGroupVersion . Version ,
Preferences : v1 . Preferences { } ,
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-15 02:07:18 +00:00
{ Name : "PINNIPED_K8S_API_ENDPOINT" , Value : v1Cluster . Server } ,
{ Name : "PINNIPED_CA_BUNDLE" , Value : string ( v1Cluster . CertificateAuthorityData ) } ,
2020-09-12 00:56:05 +00:00
{ 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 ,
}
}