1/*
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		if err != nil {
189			validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
190		} else {
191			defer clientCertCA.Close()
192		}
193	}
194
195	return validationErrors
196}
197
198// validateAuthInfo looks for conflicts and errors in the auth info
199func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error {
200	validationErrors := make([]error, 0)
201
202	usingAuthPath := false
203	methods := make([]string, 0, 3)
204	if len(authInfo.Token) != 0 {
205		methods = append(methods, "token")
206	}
207	if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
208		methods = append(methods, "basicAuth")
209	}
210
211	if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
212		// Make sure cert data and file aren't both specified
213		if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
214			validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
215		}
216		// Make sure key data and file aren't both specified
217		if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
218			validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
219		}
220		// Make sure a key is specified
221		if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
222			validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
223		}
224
225		if len(authInfo.ClientCertificate) != 0 {
226			clientCertFile, err := os.Open(authInfo.ClientCertificate)
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			} else {
230				defer clientCertFile.Close()
231			}
232		}
233		if len(authInfo.ClientKey) != 0 {
234			clientKeyFile, err := os.Open(authInfo.ClientKey)
235			if err != nil {
236				validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
237			} else {
238				defer clientKeyFile.Close()
239			}
240		}
241	}
242
243	if authInfo.Exec != nil {
244		if authInfo.AuthProvider != nil {
245			validationErrors = append(validationErrors, fmt.Errorf("authProvider cannot be provided in combination with an exec plugin for %s", authInfoName))
246		}
247		if len(authInfo.Exec.Command) == 0 {
248			validationErrors = append(validationErrors, fmt.Errorf("command must be specified for %v to use exec authentication plugin", authInfoName))
249		}
250		if len(authInfo.Exec.APIVersion) == 0 {
251			validationErrors = append(validationErrors, fmt.Errorf("apiVersion must be specified for %v to use exec authentication plugin", authInfoName))
252		}
253		for _, v := range authInfo.Exec.Env {
254			if len(v.Name) == 0 {
255				validationErrors = append(validationErrors, fmt.Errorf("env variable name must be specified for %v to use exec authentication plugin", authInfoName))
256			}
257		}
258	}
259
260	// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
261	if (len(methods) > 1) && (!usingAuthPath) {
262		validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
263	}
264
265	// ImpersonateGroups or ImpersonateUserExtra should be requested with a user
266	if (len(authInfo.ImpersonateGroups) > 0 || len(authInfo.ImpersonateUserExtra) > 0) && (len(authInfo.Impersonate) == 0) {
267		validationErrors = append(validationErrors, fmt.Errorf("requesting groups or user-extra for %v without impersonating a user", authInfoName))
268	}
269	return validationErrors
270}
271
272// 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
273func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error {
274	validationErrors := make([]error, 0)
275
276	if len(contextName) == 0 {
277		validationErrors = append(validationErrors, fmt.Errorf("empty context name for %#v is not allowed", context))
278	}
279
280	if len(context.AuthInfo) == 0 {
281		validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
282	} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
283		validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
284	}
285
286	if len(context.Cluster) == 0 {
287		validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
288	} else if _, exists := config.Clusters[context.Cluster]; !exists {
289		validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
290	}
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
298	return validationErrors
299}
300