github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/caas/kubernetes/clientconfig/k8s.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package clientconfig
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  
    12  	"github.com/juju/errors"
    13  	"github.com/juju/loggo"
    14  	"k8s.io/client-go/tools/clientcmd"
    15  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    16  
    17  	"github.com/juju/juju/cloud"
    18  )
    19  
    20  var logger = loggo.GetLogger("juju.caas.kubernetes.clientconfig")
    21  
    22  // K8sCredentialResolver defines the function for resolving non supported k8s credential.
    23  type K8sCredentialResolver func(config *clientcmdapi.Config, contextName string) (*clientcmdapi.Config, error)
    24  
    25  // EnsureK8sCredential ensures juju admin service account created with admin cluster role binding setup.
    26  func EnsureK8sCredential(config *clientcmdapi.Config, contextName string) (*clientcmdapi.Config, error) {
    27  	clientset, err := newK8sClientSet(config, contextName)
    28  	if err != nil {
    29  		return nil, errors.Trace(err)
    30  	}
    31  	return ensureJujuAdminServiceAccount(clientset, config, contextName)
    32  }
    33  
    34  // NewK8sClientConfig returns a new Kubernetes client, reading the config from the specified reader.
    35  func NewK8sClientConfig(reader io.Reader, contextName, clusterName string, credentialResolver K8sCredentialResolver) (*ClientConfig, error) {
    36  	if reader == nil {
    37  		var err error
    38  		reader, err = readKubeConfigFile()
    39  		if err != nil {
    40  			return nil, errors.Annotate(err, "failed to read Kubernetes config file")
    41  		}
    42  	}
    43  
    44  	content, err := ioutil.ReadAll(reader)
    45  	if err != nil {
    46  		return nil, errors.Annotate(err, "failed to read Kubernetes config")
    47  	}
    48  
    49  	config, err := parseKubeConfig(content)
    50  	if err != nil {
    51  		return nil, errors.Annotate(err, "failed to parse Kubernetes config")
    52  	}
    53  
    54  	contexts, err := contextsFromConfig(config)
    55  	if err != nil {
    56  		return nil, errors.Annotate(err, "failed to read contexts from kubernetes config")
    57  	}
    58  	var context Context
    59  	if contextName == "" {
    60  		contextName = config.CurrentContext
    61  	}
    62  	if clusterName != "" {
    63  		context, contextName, err = pickContextByClusterName(contexts, clusterName)
    64  		if err != nil {
    65  			return nil, errors.Annotatef(err, "picking context by cluster name %q", clusterName)
    66  		}
    67  	} else if contextName != "" {
    68  		context = contexts[contextName]
    69  		logger.Debugf("no cluster name specified, so use current context %q", config.CurrentContext)
    70  	}
    71  	// exclude not related contexts.
    72  	contexts = map[string]Context{}
    73  	if contextName != "" && !context.isEmpty() {
    74  		contexts[contextName] = context
    75  	}
    76  
    77  	// try find everything below based on context.
    78  	clouds, err := cloudsFromConfig(config, context.CloudName)
    79  	if err != nil {
    80  		return nil, errors.Annotate(err, "failed to read clouds from kubernetes config")
    81  	}
    82  
    83  	credentials, err := credentialsFromConfig(config, context.CredentialName)
    84  	if errors.IsNotSupported(err) && credentialResolver != nil {
    85  		// try to generate supported credential using provided credential.
    86  		config, err = credentialResolver(config, contextName)
    87  		if err != nil {
    88  			return nil, errors.Annotatef(
    89  				err, "ensuring k8s credential because auth info %q is not valid", context.CredentialName)
    90  		}
    91  		logger.Debugf("try again to get credentials from kubeconfig using the generated auth info")
    92  		credentials, err = credentialsFromConfig(config, context.CredentialName)
    93  	}
    94  	if err != nil {
    95  		return nil, errors.Annotate(err, "failed to read credentials from kubernetes config")
    96  	}
    97  
    98  	return &ClientConfig{
    99  		Type:           "kubernetes",
   100  		Contexts:       contexts,
   101  		CurrentContext: config.CurrentContext,
   102  		Clouds:         clouds,
   103  		Credentials:    credentials,
   104  	}, nil
   105  }
   106  
   107  func pickContextByClusterName(contexts map[string]Context, clusterName string) (Context, string, error) {
   108  	for contextName, context := range contexts {
   109  		if clusterName == context.CloudName {
   110  			return context, contextName, nil
   111  		}
   112  	}
   113  	return Context{}, "", errors.NotFoundf("context for cluster name %q", clusterName)
   114  }
   115  
   116  func contextsFromConfig(config *clientcmdapi.Config) (map[string]Context, error) {
   117  	rv := map[string]Context{}
   118  	for name, ctx := range config.Contexts {
   119  		rv[name] = Context{
   120  			CredentialName: ctx.AuthInfo,
   121  			CloudName:      ctx.Cluster,
   122  		}
   123  	}
   124  	return rv, nil
   125  }
   126  
   127  func cloudsFromConfig(config *clientcmdapi.Config, cloudName string) (map[string]CloudConfig, error) {
   128  
   129  	clusterToCloud := func(cluster *clientcmdapi.Cluster) (CloudConfig, error) {
   130  		attrs := map[string]interface{}{}
   131  
   132  		// TODO(axw) if the CA cert is specified by path, then we
   133  		// should just store the path in the cloud definition, and
   134  		// rely on cloud finalization to read it at time of use.
   135  		if cluster.CertificateAuthority != "" {
   136  			caData, err := ioutil.ReadFile(cluster.CertificateAuthority)
   137  			if err != nil {
   138  				return CloudConfig{}, errors.Trace(err)
   139  			}
   140  			cluster.CertificateAuthorityData = caData
   141  		}
   142  		attrs["CAData"] = string(cluster.CertificateAuthorityData)
   143  
   144  		return CloudConfig{
   145  			Endpoint:   cluster.Server,
   146  			Attributes: attrs,
   147  		}, nil
   148  	}
   149  
   150  	clusters := config.Clusters
   151  	if cloudName != "" {
   152  		cluster, ok := clusters[cloudName]
   153  		if !ok {
   154  			return nil, errors.NotFoundf("cluster %q", cloudName)
   155  		}
   156  		clusters = map[string]*clientcmdapi.Cluster{cloudName: cluster}
   157  	}
   158  
   159  	rv := map[string]CloudConfig{}
   160  	for name, cluster := range clusters {
   161  		c, err := clusterToCloud(cluster)
   162  		if err != nil {
   163  			return nil, errors.Trace(err)
   164  		}
   165  		rv[name] = c
   166  	}
   167  	return rv, nil
   168  }
   169  
   170  func credentialsFromConfig(config *clientcmdapi.Config, credentialName string) (map[string]cloud.Credential, error) {
   171  
   172  	authInfoToCredential := func(name string, user *clientcmdapi.AuthInfo) (cloud.Credential, error) {
   173  		logger.Debugf("name %q, user %#v", name, user)
   174  
   175  		var hasCert bool
   176  		var cred cloud.Credential
   177  		attrs := map[string]string{}
   178  
   179  		// TODO(axw) if the certificate/key are specified by path,
   180  		// then we should just store the path in the credential,
   181  		// and rely on credential finalization to read it at time
   182  		// of use.
   183  
   184  		if user.ClientCertificate != "" {
   185  			certData, err := ioutil.ReadFile(user.ClientCertificate)
   186  			if err != nil {
   187  				return cred, errors.Trace(err)
   188  			}
   189  			user.ClientCertificateData = certData
   190  		}
   191  
   192  		if user.ClientKey != "" {
   193  			keyData, err := ioutil.ReadFile(user.ClientKey)
   194  			if err != nil {
   195  				return cred, errors.Trace(err)
   196  			}
   197  			user.ClientKeyData = keyData
   198  		}
   199  
   200  		if len(user.ClientCertificateData) > 0 {
   201  			attrs["ClientCertificateData"] = string(user.ClientCertificateData)
   202  			hasCert = true
   203  		}
   204  		hasClientKeyData := len(user.ClientKeyData) > 0
   205  		if hasClientKeyData {
   206  			attrs["ClientKeyData"] = string(user.ClientKeyData)
   207  		}
   208  		hasToken := user.Token != ""
   209  		if hasToken {
   210  			if user.Username != "" || user.Password != "" {
   211  				return cred, errors.NotValidf("AuthInfo: %q with both Token and User/Pass", name)
   212  			}
   213  			attrs["Token"] = user.Token
   214  		}
   215  
   216  		var authType cloud.AuthType
   217  		if hasClientKeyData {
   218  			// auth type used for aks for example.
   219  			authType = cloud.OAuth2AuthType
   220  			if hasCert {
   221  				authType = cloud.OAuth2WithCertAuthType
   222  			}
   223  			if !hasToken {
   224  				// the Token is required.
   225  				return cred, errors.NotValidf("missing token for %q with auth type %q", name, authType)
   226  			}
   227  		} else if user.Username != "" {
   228  			// basic auth type.
   229  			if user.Password == "" {
   230  				logger.Debugf("credential for user %q has empty password", user.Username)
   231  			}
   232  			attrs["username"] = user.Username
   233  			attrs["password"] = user.Password
   234  			if hasCert {
   235  				authType = cloud.UserPassWithCertAuthType
   236  			} else {
   237  				authType = cloud.UserPassAuthType
   238  			}
   239  		} else if hasCert && hasToken {
   240  			// bearer token of service account auth type gke for example.
   241  			authType = cloud.CertificateAuthType
   242  		} else {
   243  			return cred, errors.NotSupportedf("configuration for %q", name)
   244  		}
   245  
   246  		cred = cloud.NewCredential(authType, attrs)
   247  		cred.Label = fmt.Sprintf("kubernetes credential %q", name)
   248  		return cred, nil
   249  	}
   250  
   251  	authInfos := config.AuthInfos
   252  	if credentialName != "" {
   253  		authInfo, ok := authInfos[credentialName]
   254  		if !ok {
   255  			return nil, errors.NotFoundf("authInfo %q", credentialName)
   256  		}
   257  		authInfos = map[string]*clientcmdapi.AuthInfo{credentialName: authInfo}
   258  	}
   259  	rv := map[string]cloud.Credential{}
   260  	for name, user := range authInfos {
   261  		cred, err := authInfoToCredential(name, user)
   262  		if err != nil {
   263  			return nil, errors.Trace(err)
   264  		}
   265  		rv[name] = cred
   266  	}
   267  	return rv, nil
   268  }
   269  
   270  // GetKubeConfigPath - define kubeconfig file path to use
   271  func GetKubeConfigPath() string {
   272  	kubeconfig := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
   273  	if kubeconfig == "" {
   274  		kubeconfig = clientcmd.RecommendedHomeFile
   275  	}
   276  	logger.Debugf("The kubeconfig file path: %q", kubeconfig)
   277  	return kubeconfig
   278  }
   279  
   280  func readKubeConfigFile() (reader io.Reader, err error) {
   281  	// Try to read from kubeconfig file.
   282  	filename := GetKubeConfigPath()
   283  	reader, err = os.Open(filename)
   284  	if err != nil {
   285  		if os.IsNotExist(err) {
   286  			return nil, errors.NotFoundf(filename)
   287  		}
   288  		return nil, errors.Trace(errors.Annotatef(err, "failed to read kubernetes config from '%s'", filename))
   289  	}
   290  	return reader, nil
   291  }
   292  
   293  func parseKubeConfig(data []byte) (*clientcmdapi.Config, error) {
   294  
   295  	config, err := clientcmd.Load(data)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	if config.AuthInfos == nil {
   301  		config.AuthInfos = map[string]*clientcmdapi.AuthInfo{}
   302  	}
   303  	if config.Clusters == nil {
   304  		config.Clusters = map[string]*clientcmdapi.Cluster{}
   305  	}
   306  	if config.Contexts == nil {
   307  		config.Contexts = map[string]*clientcmdapi.Context{}
   308  	}
   309  
   310  	return config, nil
   311  }