blob: f77ef04fe54d630bd743c197463a5072e6e4c757 [file] [log] [blame]
Matteo Scandoloa4285862020-12-01 18:10:10 -08001/*
2Copyright 2014 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package clientcmd
18
19import (
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
31var (
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
39func NewEmptyConfigError(message string) error {
40 return &errEmptyConfig{message}
41}
42
43type errEmptyConfig struct {
44 message string
45}
46
47func (e *errEmptyConfig) Error() string {
48 return e.message
49}
50
51type errContextNotFound struct {
52 ContextName string
53}
54
55func (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
61func 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.
73func 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.
87type errConfigurationInvalid []error
88
89// errConfigurationInvalid implements error and Aggregate
90var _ error = errConfigurationInvalid{}
91var _ utilerrors.Aggregate = errConfigurationInvalid{}
92
93func 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
103func (e errConfigurationInvalid) Error() string {
104 return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
105}
106
107// Errors implements the utilerrors.Aggregate interface
108func (e errConfigurationInvalid) Errors() []error {
109 return e
110}
111
112// Is implements the utilerrors.Aggregate interface
113func (e errConfigurationInvalid) Is(target error) bool {
114 return e.visit(func(err error) bool {
115 return errors.Is(err, target)
116 })
117}
118
119func (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.
143func 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.
152func 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.
182func 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
215func 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
252func 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 }
312
313 // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
314 if (len(methods) > 1) && (!usingAuthPath) {
315 validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
316 }
317
318 // ImpersonateGroups or ImpersonateUserExtra should be requested with a user
319 if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) {
320 validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName))
321 }
322 return validationErrors
323}
324
325// 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
326func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
327 validationErrors := make([]error, 0)
328
329 if len(contextName) == 0 {
330 validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context))
331 }
332
333 if len(context.AuthInfo) == 0 {
334 validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
335 } else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
336 validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
337 }
338
339 if len(context.Cluster) == 0 {
340 validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
341 } else if _, exists := config.Clusters[context.Cluster]; !exists {
342 validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
343 }
344
345 if len(context.Namespace) != 0 {
346 if len(validation.IsDNS1123Label(context.Namespace)) != 0 {
347 validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS_LABEL rules", context.Namespace, contextName))
348 }
349 }
350
351 return validationErrors
352}