...

Source file src/k8s.io/client-go/tools/clientcmd/validation.go

Documentation: k8s.io/client-go/tools/clientcmd

     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  }
   372  

View as plain text