2
0

validation.go 14 KB

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