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