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  }