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		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}
299