validation.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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 = NewEmptyConfigError("no configuration has been provided, try setting KUBERNETES_MASTER environment variable")
  27. // message is for consistency with old behavior
  28. ErrEmptyCluster = errors.New("cluster has no server defined")
  29. )
  30. // NewEmptyConfigError returns an error wrapping the given message which IsEmptyConfig() will recognize as an empty config error
  31. func NewEmptyConfigError(message string) error {
  32. return &errEmptyConfig{message}
  33. }
  34. type errEmptyConfig struct {
  35. message string
  36. }
  37. func (e *errEmptyConfig) Error() string {
  38. return e.message
  39. }
  40. type errContextNotFound struct {
  41. ContextName string
  42. }
  43. func (e *errContextNotFound) Error() string {
  44. return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
  45. }
  46. // IsContextNotFound returns a boolean indicating whether the error is known to
  47. // report that a context was not found
  48. func IsContextNotFound(err error) bool {
  49. if err == nil {
  50. return false
  51. }
  52. if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext {
  53. return true
  54. }
  55. return strings.Contains(err.Error(), "context was not found for specified context")
  56. }
  57. // IsEmptyConfig returns true if the provided error indicates the provided configuration
  58. // is empty.
  59. func IsEmptyConfig(err error) bool {
  60. switch t := err.(type) {
  61. case errConfigurationInvalid:
  62. if len(t) != 1 {
  63. return false
  64. }
  65. _, ok := t[0].(*errEmptyConfig)
  66. return ok
  67. }
  68. _, ok := err.(*errEmptyConfig)
  69. return ok
  70. }
  71. // errConfigurationInvalid is a set of errors indicating the configuration is invalid.
  72. type errConfigurationInvalid []error
  73. // errConfigurationInvalid implements error and Aggregate
  74. var _ error = errConfigurationInvalid{}
  75. var _ utilerrors.Aggregate = errConfigurationInvalid{}
  76. func newErrConfigurationInvalid(errs []error) error {
  77. switch len(errs) {
  78. case 0:
  79. return nil
  80. default:
  81. return errConfigurationInvalid(errs)
  82. }
  83. }
  84. // Error implements the error interface
  85. func (e errConfigurationInvalid) Error() string {
  86. return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
  87. }
  88. // Errors implements the utilerrors.Aggregate interface
  89. func (e errConfigurationInvalid) Errors() []error {
  90. return e
  91. }
  92. // Is implements the utilerrors.Aggregate interface
  93. func (e errConfigurationInvalid) Is(target error) bool {
  94. return e.visit(func(err error) bool {
  95. return errors.Is(err, target)
  96. })
  97. }
  98. func (e errConfigurationInvalid) visit(f func(err error) bool) bool {
  99. for _, err := range e {
  100. switch err := err.(type) {
  101. case errConfigurationInvalid:
  102. if match := err.visit(f); match {
  103. return match
  104. }
  105. case utilerrors.Aggregate:
  106. for _, nestedErr := range err.Errors() {
  107. if match := f(nestedErr); match {
  108. return match
  109. }
  110. }
  111. default:
  112. if match := f(err); match {
  113. return match
  114. }
  115. }
  116. }
  117. return false
  118. }
  119. // IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid.
  120. func IsConfigurationInvalid(err error) bool {
  121. switch err.(type) {
  122. case *errContextNotFound, errConfigurationInvalid:
  123. return true
  124. }
  125. return IsContextNotFound(err)
  126. }
  127. // Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible.
  128. func Validate(config clientcmdapi.Config) error {
  129. validationErrors := make([]error, 0)
  130. if clientcmdapi.IsConfigEmpty(&config) {
  131. return newErrConfigurationInvalid([]error{ErrEmptyConfig})
  132. }
  133. if len(config.CurrentContext) != 0 {
  134. if _, exists := config.Contexts[config.CurrentContext]; !exists {
  135. validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
  136. }
  137. }
  138. for contextName, context := range config.Contexts {
  139. validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
  140. }
  141. for authInfoName, authInfo := range config.AuthInfos {
  142. validationErrors = append(validationErrors, validateAuthInfo(authInfoName, *authInfo)...)
  143. }
  144. for clusterName, clusterInfo := range config.Clusters {
  145. validationErrors = append(validationErrors, validateClusterInfo(clusterName, *clusterInfo)...)
  146. }
  147. return newErrConfigurationInvalid(validationErrors)
  148. }
  149. // ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
  150. // but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
  151. func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error {
  152. validationErrors := make([]error, 0)
  153. if clientcmdapi.IsConfigEmpty(&config) {
  154. return newErrConfigurationInvalid([]error{ErrEmptyConfig})
  155. }
  156. var contextName string
  157. if len(passedContextName) != 0 {
  158. contextName = passedContextName
  159. } else {
  160. contextName = config.CurrentContext
  161. }
  162. if len(contextName) == 0 {
  163. return ErrNoContext
  164. }
  165. context, exists := config.Contexts[contextName]
  166. if !exists {
  167. validationErrors = append(validationErrors, &errContextNotFound{contextName})
  168. }
  169. if exists {
  170. validationErrors = append(validationErrors, validateContext(contextName, *context, config)...)
  171. validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, *config.AuthInfos[context.AuthInfo])...)
  172. validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, *config.Clusters[context.Cluster])...)
  173. }
  174. return newErrConfigurationInvalid(validationErrors)
  175. }
  176. // validateClusterInfo looks for conflicts and errors in the cluster info
  177. func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error {
  178. validationErrors := make([]error, 0)
  179. emptyCluster := clientcmdapi.NewCluster()
  180. if reflect.DeepEqual(*emptyCluster, clusterInfo) {
  181. return []error{ErrEmptyCluster}
  182. }
  183. if len(clusterInfo.Server) == 0 {
  184. if len(clusterName) == 0 {
  185. validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined"))
  186. } else {
  187. validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
  188. }
  189. }
  190. if proxyURL := clusterInfo.ProxyURL; proxyURL != "" {
  191. if _, err := parseProxyURL(proxyURL); err != nil {
  192. validationErrors = append(validationErrors, fmt.Errorf("invalid 'proxy-url' %q for cluster %q: %v", proxyURL, clusterName, err))
  193. }
  194. }
  195. // Make sure CA data and CA file aren't both specified
  196. if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
  197. validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
  198. }
  199. if len(clusterInfo.CertificateAuthority) != 0 {
  200. clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
  201. if err != nil {
  202. validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
  203. } else {
  204. defer clientCertCA.Close()
  205. }
  206. }
  207. return validationErrors
  208. }
  209. // validateAuthInfo looks for conflicts and errors in the auth info
  210. func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error {
  211. validationErrors := make([]error, 0)
  212. usingAuthPath := false
  213. methods := make([]string, 0, 3)
  214. if len(authInfo.Token) != 0 {
  215. methods = append(methods, "token")
  216. }
  217. if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
  218. methods = append(methods, "basicAuth")
  219. }
  220. if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
  221. // Make sure cert data and file aren't both specified
  222. if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
  223. validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
  224. }
  225. // Make sure key data and file aren't both specified
  226. if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
  227. validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
  228. }
  229. // Make sure a key is specified
  230. if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
  231. validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
  232. }
  233. if len(authInfo.ClientCertificate) != 0 {
  234. clientCertFile, err := os.Open(authInfo.ClientCertificate)
  235. if err != nil {
  236. validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
  237. } else {
  238. defer clientCertFile.Close()
  239. }
  240. }
  241. if len(authInfo.ClientKey) != 0 {
  242. clientKeyFile, err := os.Open(authInfo.ClientKey)
  243. if err != nil {
  244. validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
  245. } else {
  246. defer clientKeyFile.Close()
  247. }
  248. }
  249. }
  250. if authInfo.Exec != nil {
  251. if authInfo.AuthProvider != nil {
  252. validationErrors = append(validationErrors, fmt.Errorf("authProvider cannot be provided in combination with an exec plugin for %s", authInfoName))
  253. }
  254. if len(authInfo.Exec.Command) == 0 {
  255. validationErrors = append(validationErrors, fmt.Errorf("command must be specified for %v to use exec authentication plugin", authInfoName))
  256. }
  257. if len(authInfo.Exec.APIVersion) == 0 {
  258. validationErrors = append(validationErrors, fmt.Errorf("apiVersion must be specified for %v to use exec authentication plugin", authInfoName))
  259. }
  260. for _, v := range authInfo.Exec.Env {
  261. if len(v.Name) == 0 {
  262. validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName))
  263. }
  264. }
  265. }
  266. // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
  267. if (len(methods) > 1) && (!usingAuthPath) {
  268. validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
  269. }
  270. // ImpersonateGroups or ImpersonateUserExtra should be requested with a user
  271. if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) {
  272. validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName))
  273. }
  274. return validationErrors
  275. }
  276. // 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
  277. func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
  278. validationErrors := make([]error, 0)
  279. if len(contextName) == 0 {
  280. validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context))
  281. }
  282. if len(context.AuthInfo) == 0 {
  283. validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
  284. } else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
  285. validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
  286. }
  287. if len(context.Cluster) == 0 {
  288. validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
  289. } else if _, exists := config.Clusters[context.Cluster]; !exists {
  290. validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
  291. }
  292. if len(context.Namespace) != 0 {
  293. if len(validation.IsDNS1123Label(context.Namespace)) != 0 {
  294. validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName))
  295. }
  296. }
  297. return validationErrors
  298. }