1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package clientcmd 18 19import ( 20 "errors" 21 "fmt" 22 "os" 23 "reflect" 24 "strings" 25 26 utilerrors "k8s.io/apimachinery/pkg/util/errors" 27 "k8s.io/apimachinery/pkg/util/validation" 28 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 29) 30 31var ( 32 ErrNoContext = errors.New("no context chosen") 33 ErrEmptyConfig = errors.New("no configuration has been provided") 34 // message is for consistency with old behavior 35 ErrEmptyCluster = errors.New("cluster has no server defined") 36) 37 38type errContextNotFound struct { 39 ContextName string 40} 41 42func (e *errContextNotFound) Error() string { 43 return fmt.Sprintf("context was not found for specified context: %v", e.ContextName) 44} 45 46// IsContextNotFound returns a boolean indicating whether the error is known to 47// report that a context was not found 48func IsContextNotFound(err error) bool { 49 if err == nil { 50 return false 51 } 52 if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext { 53 return true 54 } 55 return strings.Contains(err.Error(), "context was not found for specified context") 56} 57 58// IsEmptyConfig returns true if the provided error indicates the provided configuration 59// is empty. 60func IsEmptyConfig(err error) bool { 61 switch t := err.(type) { 62 case errConfigurationInvalid: 63 return len(t) == 1 && t[0] == ErrEmptyConfig 64 } 65 return err == ErrEmptyConfig 66} 67 68// errConfigurationInvalid is a set of errors indicating the configuration is invalid. 69type errConfigurationInvalid []error 70 71// errConfigurationInvalid implements error and Aggregate 72var _ error = errConfigurationInvalid{} 73var _ utilerrors.Aggregate = errConfigurationInvalid{} 74 75func newErrConfigurationInvalid(errs []error) error { 76 switch len(errs) { 77 case 0: 78 return nil 79 default: 80 return errConfigurationInvalid(errs) 81 } 82} 83 84// Error implements the error interface 85func (e errConfigurationInvalid) Error() string { 86 return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error()) 87} 88 89// Errors implements the AggregateError interface 90func (e errConfigurationInvalid) Errors() []error { 91 return e 92} 93 94// IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid. 95func IsConfigurationInvalid(err error) bool { 96 switch err.(type) { 97 case *errContextNotFound, errConfigurationInvalid: 98 return true 99 } 100 return IsContextNotFound(err) 101} 102 103// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible. 104func Validate(config clientcmdapi.Config) error { 105 validationErrors := make([]error, 0) 106 107 if clientcmdapi.IsConfigEmpty(&config) { 108 return newErrConfigurationInvalid([]error{ErrEmptyConfig}) 109 } 110 111 if len(config.CurrentContext) != 0 { 112 if _, exists := config.Contexts[config.CurrentContext]; !exists { 113 validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext}) 114 } 115 } 116 117 for contextName, context := range config.Contexts { 118 validationErrors = append(validationErrors, validateContext(contextName, *context, config)...) 119 } 120 121 for authInfoName, authInfo := range config.AuthInfos { 122 validationErrors = append(validationErrors, validateAuthInfo(authInfoName, *authInfo)...) 123 } 124 125 for clusterName, clusterInfo := range config.Clusters { 126 validationErrors = append(validationErrors, validateClusterInfo(clusterName, *clusterInfo)...) 127 } 128 129 return newErrConfigurationInvalid(validationErrors) 130} 131 132// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config, 133// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible. 134func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error { 135 validationErrors := make([]error, 0) 136 137 if clientcmdapi.IsConfigEmpty(&config) { 138 return newErrConfigurationInvalid([]error{ErrEmptyConfig}) 139 } 140 141 var contextName string 142 if len(passedContextName) != 0 { 143 contextName = passedContextName 144 } else { 145 contextName = config.CurrentContext 146 } 147 148 if len(contextName) == 0 { 149 return ErrNoContext 150 } 151 152 context, exists := config.Contexts[contextName] 153 if !exists { 154 validationErrors = append(validationErrors, &errContextNotFound{contextName}) 155 } 156 157 if exists { 158 validationErrors = append(validationErrors, validateContext(contextName, *context, config)...) 159 validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *config.AuthInfos[context.AuthInfo])...) 160 validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *config.Clusters[context.Cluster])...) 161 } 162 163 return newErrConfigurationInvalid(validationErrors) 164} 165 166// validateClusterInfo looks for conflicts and errors in the cluster info 167func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error { 168 validationErrors := make([]error, 0) 169 170 emptyCluster := clientcmdapi.NewCluster() 171 if reflect.DeepEqual(*emptyCluster, clusterInfo) { 172 return []error{ErrEmptyCluster} 173 } 174 175 if len(clusterInfo.Server) == 0 { 176 if len(clusterName) == 0 { 177 validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined")) 178 } else { 179 validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName)) 180 } 181 } 182 // Make sure CA data and CA file aren't both specified 183 if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 { 184 validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName)) 185 } 186 if len(clusterInfo.CertificateAuthority) != 0 { 187 clientCertCA, err := os.Open(clusterInfo.CertificateAuthority) 188 if err != nil { 189 validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err)) 190 } else { 191 defer clientCertCA.Close() 192 } 193 } 194 195 return validationErrors 196} 197 198// validateAuthInfo looks for conflicts and errors in the auth info 199func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error { 200 validationErrors := make([]error, 0) 201 202 usingAuthPath := false 203 methods := make([]string, 0, 3) 204 if len(authInfo.Token) != 0 { 205 methods = append(methods, "token") 206 } 207 if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 { 208 methods = append(methods, "basicAuth") 209 } 210 211 if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 { 212 // Make sure cert data and file aren't both specified 213 if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 { 214 validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName)) 215 } 216 // Make sure key data and file aren't both specified 217 if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 { 218 validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName)) 219 } 220 // Make sure a key is specified 221 if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 { 222 validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName)) 223 } 224 225 if len(authInfo.ClientCertificate) != 0 { 226 clientCertFile, err := os.Open(authInfo.ClientCertificate) 227 if err != nil { 228 validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err)) 229 } else { 230 defer clientCertFile.Close() 231 } 232 } 233 if len(authInfo.ClientKey) != 0 { 234 clientKeyFile, err := os.Open(authInfo.ClientKey) 235 if err != nil { 236 validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err)) 237 } else { 238 defer clientKeyFile.Close() 239 } 240 } 241 } 242 243 if authInfo.Exec != nil { 244 if authInfo.AuthProvider != nil { 245 validationErrors = append(validationErrors, fmt.Errorf("authProvider cannot be provided in combination with an exec plugin for %s", authInfoName)) 246 } 247 if len(authInfo.Exec.Command) == 0 { 248 validationErrors = append(validationErrors, fmt.Errorf("command must be specified for %v to use exec authentication plugin", authInfoName)) 249 } 250 if len(authInfo.Exec.APIVersion) == 0 { 251 validationErrors = append(validationErrors, fmt.Errorf("apiVersion must be specified for %v to use exec authentication plugin", authInfoName)) 252 } 253 for _, v := range authInfo.Exec.Env { 254 if len(v.Name) == 0 { 255 validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName)) 256 } 257 } 258 } 259 260 // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case 261 if (len(methods) > 1) && (!usingAuthPath) { 262 validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods)) 263 } 264 265 // ImpersonateGroups or ImpersonateUserExtra should be requested with a user 266 if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) { 267 validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName)) 268 } 269 return validationErrors 270} 271 272// validateContext looks for errors in the context. It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return 273func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error { 274 validationErrors := make([]error, 0) 275 276 if len(contextName) == 0 { 277 validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context)) 278 } 279 280 if len(context.AuthInfo) == 0 { 281 validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName)) 282 } else if _, exists := config.AuthInfos[context.AuthInfo]; !exists { 283 validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName)) 284 } 285 286 if len(context.Cluster) == 0 { 287 validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName)) 288 } else if _, exists := config.Clusters[context.Cluster]; !exists { 289 validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName)) 290 } 291 292 if len(context.Namespace) != 0 { 293 if len(validation.IsDNS1123Label(context.Namespace)) != 0 { 294 validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName)) 295 } 296 } 297 298 return validationErrors 299} 300