validation.go 11 KB

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