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