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 }