k8s.io/client-go@v0.31.1/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 208 // Default to empty users and clusters and let the validation function report an error. 209 authInfo := config.AuthInfos[context.AuthInfo] 210 if authInfo == nil { 211 authInfo = &clientcmdapi.AuthInfo{} 212 } 213 validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *authInfo)...) 214 215 cluster := config.Clusters[context.Cluster] 216 if cluster == nil { 217 cluster = &clientcmdapi.Cluster{} 218 } 219 validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *cluster)...) 220 } 221 222 return newErrConfigurationInvalid(validationErrors) 223 } 224 225 // validateClusterInfo looks for conflicts and errors in the cluster info 226 func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error { 227 validationErrors := make([]error, 0) 228 229 emptyCluster := clientcmdapi.NewCluster() 230 if reflect.DeepEqual(*emptyCluster, clusterInfo) { 231 return []error{ErrEmptyCluster} 232 } 233 234 if len(clusterInfo.Server) == 0 { 235 if len(clusterName) == 0 { 236 validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined")) 237 } else { 238 validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName)) 239 } 240 } 241 if proxyURL := clusterInfo.ProxyURL; proxyURL != "" { 242 if _, err := parseProxyURL(proxyURL); err != nil { 243 validationErrors = append(validationErrors, fmt.Errorf("invalid 'proxy-url' %q for cluster %q: %w", proxyURL, clusterName, err)) 244 } 245 } 246 // Make sure CA data and CA file aren't both specified 247 if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 { 248 validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName)) 249 } 250 if len(clusterInfo.CertificateAuthority) != 0 { 251 clientCertCA, err := os.Open(clusterInfo.CertificateAuthority) 252 if err != nil { 253 validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %w", clusterInfo.CertificateAuthority, clusterName, err)) 254 } else { 255 defer clientCertCA.Close() 256 } 257 } 258 259 return validationErrors 260 } 261 262 // validateAuthInfo looks for conflicts and errors in the auth info 263 func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error { 264 validationErrors := make([]error, 0) 265 266 usingAuthPath := false 267 methods := make([]string, 0, 3) 268 if len(authInfo.Token) != 0 { 269 methods = append(methods, "token") 270 } 271 if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 { 272 methods = append(methods, "basicAuth") 273 } 274 275 if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 { 276 // Make sure cert data and file aren't both specified 277 if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 { 278 validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName)) 279 } 280 // Make sure key data and file aren't both specified 281 if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 { 282 validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName)) 283 } 284 // Make sure a key is specified 285 if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 { 286 validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName)) 287 } 288 289 if len(authInfo.ClientCertificate) != 0 { 290 clientCertFile, err := os.Open(authInfo.ClientCertificate) 291 if err != nil { 292 validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %w", authInfo.ClientCertificate, authInfoName, err)) 293 } else { 294 defer clientCertFile.Close() 295 } 296 } 297 if len(authInfo.ClientKey) != 0 { 298 clientKeyFile, err := os.Open(authInfo.ClientKey) 299 if err != nil { 300 validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %w", authInfo.ClientKey, authInfoName, err)) 301 } else { 302 defer clientKeyFile.Close() 303 } 304 } 305 } 306 307 if authInfo.Exec != nil { 308 if authInfo.AuthProvider != nil { 309 validationErrors = append(validationErrors, fmt.Errorf("authProvider cannot be provided in combination with an exec plugin for %s", authInfoName)) 310 } 311 if len(authInfo.Exec.Command) == 0 { 312 validationErrors = append(validationErrors, fmt.Errorf("command must be specified for %v to use exec authentication plugin", authInfoName)) 313 } 314 if len(authInfo.Exec.APIVersion) == 0 { 315 validationErrors = append(validationErrors, fmt.Errorf("apiVersion must be specified for %v to use exec authentication plugin", authInfoName)) 316 } 317 for _, v := range authInfo.Exec.Env { 318 if len(v.Name) == 0 { 319 validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName)) 320 } 321 } 322 switch authInfo.Exec.InteractiveMode { 323 case "": 324 validationErrors = append(validationErrors, fmt.Errorf("interactiveMode must be specified for %v to use exec authentication plugin", authInfoName)) 325 case clientcmdapi.NeverExecInteractiveMode, clientcmdapi.IfAvailableExecInteractiveMode, clientcmdapi.AlwaysExecInteractiveMode: 326 // These are valid 327 default: 328 validationErrors = append(validationErrors, fmt.Errorf("invalid interactiveMode for %v: %q", authInfoName, authInfo.Exec.InteractiveMode)) 329 } 330 } 331 332 // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case 333 if (len(methods) > 1) && (!usingAuthPath) { 334 validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods)) 335 } 336 337 // ImpersonateUID, ImpersonateGroups or ImpersonateUserExtra should be requested with a user 338 if (len(authInfo.ImpersonateUID) > 0 || len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) { 339 validationErrors = append(validationErrors, fmt.Errorf("requesting uid, groups or user-extra for %v without impersonating a user", authInfoName)) 340 } 341 return validationErrors 342 } 343 344 // 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 345 func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error { 346 validationErrors := make([]error, 0) 347 348 if len(contextName) == 0 { 349 validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context)) 350 } 351 352 if len(context.AuthInfo) == 0 { 353 validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName)) 354 } else if _, exists := config.AuthInfos[context.AuthInfo]; !exists { 355 validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName)) 356 } 357 358 if len(context.Cluster) == 0 { 359 validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName)) 360 } else if _, exists := config.Clusters[context.Cluster]; !exists { 361 validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName)) 362 } 363 364 if len(context.Namespace) != 0 { 365 if len(validation.IsDNS1123Label(context.Namespace)) != 0 { 366 validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName)) 367 } 368 } 369 370 return validationErrors 371 }