Merge pull request #6338 from smarterclayton/improve_config_error

Return a typed error for config validation, and make errors simple
This commit is contained in:
Yu-Ju Hong
2015-04-03 18:08:15 -07:00
9 changed files with 236 additions and 83 deletions

View File

@@ -26,7 +26,6 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
)
var (
@@ -261,7 +260,7 @@ func (config DirectClientConfig) ConfirmUsable() error {
validationErrors = append(validationErrors, validateAuthInfo(config.getAuthInfoName(), config.getAuthInfo())...)
validationErrors = append(validationErrors, validateClusterInfo(config.getClusterName(), config.getCluster())...)
return errors.NewAggregate(validationErrors)
return newErrConfigurationInvalid(validationErrors)
}
func (config DirectClientConfig) getContextName() string {

View File

@@ -43,10 +43,47 @@ func IsContextNotFound(err error) bool {
if err == nil {
return false
}
if _, ok := err.(*errContextNotFound); ok || err == ErrNoContext {
return true
}
return strings.Contains(err.Error(), "context was not found for specified context")
}
// errConfigurationInvalid is a set of errors indicating the configuration is invalid.
type errConfigurationInvalid []error
// errConfigurationInvalid implements error and Aggregate
var _ error = errConfigurationInvalid{}
var _ utilerrors.Aggregate = errConfigurationInvalid{}
func newErrConfigurationInvalid(errs []error) error {
switch len(errs) {
case 0:
return nil
default:
return errConfigurationInvalid(errs)
}
}
// Error implements the error interface
func (e errConfigurationInvalid) Error() string {
return fmt.Sprintf("invalid configuration: %v", utilerrors.NewAggregate(e).Error())
}
// Errors implements the AggregateError interface
func (e errConfigurationInvalid) Errors() []error {
return e
}
// IsConfigurationInvalid returns true if the provided error indicates the configuration is invalid.
func IsConfigurationInvalid(err error) bool {
switch err.(type) {
case *errContextNotFound, errConfigurationInvalid:
return true
}
return IsContextNotFound(err)
}
// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible.
func Validate(config clientcmdapi.Config) error {
validationErrors := make([]error, 0)
@@ -69,7 +106,7 @@ func Validate(config clientcmdapi.Config) error {
validationErrors = append(validationErrors, validateClusterInfo(clusterName, clusterInfo)...)
}
return utilerrors.NewAggregate(validationErrors)
return newErrConfigurationInvalid(validationErrors)
}
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
@@ -99,7 +136,7 @@ func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error {
validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, config.Clusters[context.Cluster])...)
}
return utilerrors.NewAggregate(validationErrors)
return newErrConfigurationInvalid(validationErrors)
}
// validateClusterInfo looks for conflicts and errors in the cluster info
@@ -107,7 +144,11 @@ func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) [
validationErrors := make([]error, 0)
if len(clusterInfo.Server) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("no server found for %v", clusterName))
if len(clusterName) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("default cluster has no server defined"))
} else {
validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
}
}
// Make sure CA data and CA file aren't both specified
if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
@@ -155,7 +196,7 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err
}
// Make sure key data and file aren't both specified
if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v. client-key-data will override.", authInfoName))
validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v; client-key-data will override", authInfoName))
}
// Make sure a key is specified
if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
@@ -180,7 +221,7 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err
// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
if (len(methods) > 1) && (!usingAuthPath) {
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v. Found %v, only one is allowed", authInfoName, methods))
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v; found %v, only one is allowed", authInfoName, methods))
}
return validationErrors
@@ -191,19 +232,19 @@ func validateContext(contextName string, context clientcmdapi.Context, config cl
validationErrors := make([]error, 0)
if len(context.AuthInfo) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("user was not specified for Context %v", contextName))
validationErrors = append(validationErrors, fmt.Errorf("user was not specified for context %q", contextName))
} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
validationErrors = append(validationErrors, fmt.Errorf("user, %v, was not found for Context %v", context.AuthInfo, contextName))
validationErrors = append(validationErrors, fmt.Errorf("user %q was not found for context %q", context.AuthInfo, contextName))
}
if len(context.Cluster) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for Context %v", contextName))
validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for context %q", contextName))
} else if _, exists := config.Clusters[context.Cluster]; !exists {
validationErrors = append(validationErrors, fmt.Errorf("cluster, %v, was not found for Context %v", context.Cluster, contextName))
validationErrors = append(validationErrors, fmt.Errorf("cluster %q was not found for context %q", context.Cluster, contextName))
}
if (len(context.Namespace) != 0) && !util.IsDNS952Label(context.Namespace) {
validationErrors = append(validationErrors, fmt.Errorf("namespace, %v, for context %v, does not conform to the kubernetes DNS952 rules", context.Namespace, contextName))
validationErrors = append(validationErrors, fmt.Errorf("namespace %q for context %q does not conform to the kubernetes DNS952 rules", context.Namespace, contextName))
}
return validationErrors

View File

@@ -127,14 +127,33 @@ func TestIsContextNotFound(t *testing.T) {
if !IsContextNotFound(err) {
t.Errorf("Expected context not found, but got %v", err)
}
if !IsConfigurationInvalid(err) {
t.Errorf("Expected configuration invalid, but got %v", err)
}
}
func TestIsConfigurationInvalid(t *testing.T) {
if newErrConfigurationInvalid([]error{}) != nil {
t.Errorf("unexpected error")
}
if newErrConfigurationInvalid([]error{ErrNoContext}) == ErrNoContext {
t.Errorf("unexpected error")
}
if newErrConfigurationInvalid([]error{ErrNoContext, ErrNoContext}) == nil {
t.Errorf("unexpected error")
}
if !IsConfigurationInvalid(newErrConfigurationInvalid([]error{ErrNoContext, ErrNoContext})) {
t.Errorf("unexpected error")
}
}
func TestValidateMissingReferencesConfig(t *testing.T) {
config := clientcmdapi.NewConfig()
config.CurrentContext = "anything"
config.Contexts["anything"] = clientcmdapi.Context{Cluster: "missing", AuthInfo: "missing"}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"user, missing, was not found for Context anything", "cluster, missing, was not found for Context anything"},
expectedErrorSubstring: []string{"user \"missing\" was not found for context \"anything\"", "cluster \"missing\" was not found for context \"anything\""},
}
test.testContext("anything", t)
@@ -146,7 +165,7 @@ func TestValidateEmptyContext(t *testing.T) {
config.Contexts["anything"] = clientcmdapi.Context{}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"user was not specified for Context anything", "cluster was not specified for Context anything"},
expectedErrorSubstring: []string{"user was not specified for context \"anything\"", "cluster was not specified for context \"anything\""},
}
test.testContext("anything", t)
@@ -377,6 +396,9 @@ func (c configValidationTest) testConfig(t *testing.T) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
}
}
if !IsConfigurationInvalid(err) {
t.Errorf("all errors should be configuration invalid: %v", err)
}
}
} else {
if err != nil {

View File

@@ -52,25 +52,6 @@ type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// UnexpectedStatusError is returned as an error if a response's body and HTTP code don't
// make sense together.
type UnexpectedStatusError struct {
Request *http.Request
Response *http.Response
Body string
}
// Error returns a textual description of 'u'.
func (u *UnexpectedStatusError) Error() string {
return fmt.Sprintf("request [%+v] failed (%d) %s: %s", u.Request, u.Response.StatusCode, u.Response.Status, u.Body)
}
// IsUnexpectedStatusError determines if err is due to an unexpected status from the server.
func IsUnexpectedStatusError(err error) bool {
_, ok := err.(*UnexpectedStatusError)
return ok
}
// RequestConstructionError is returned when there's an error assembling a request.
type RequestConstructionError struct {
Err error
@@ -689,7 +670,6 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request, body
// initial contact, the presence of mismatched body contents from posted content types
// - Give these a separate distinct error type and capture as much as possible of the original message
//
// TODO: introduce further levels of refinement that allow a client to distinguish between 1 and 2-3.
// TODO: introduce transformation of generic http.Client.Do() errors that separates 4.
func (r *Request) transformUnstructuredResponseError(resp *http.Response, req *http.Request, body []byte) error {
if body == nil && resp.Body != nil {
@@ -697,43 +677,12 @@ func (r *Request) transformUnstructuredResponseError(resp *http.Response, req *h
body = data
}
}
var err error = &UnexpectedStatusError{
Request: req,
Response: resp,
Body: string(body),
}
message := "unknown"
if isTextResponse(resp) {
message = strings.TrimSpace(string(body))
}
// TODO: handle other error classes we know about
switch resp.StatusCode {
case http.StatusConflict:
if req.Method == "POST" {
err = errors.NewAlreadyExists(r.resource, r.resourceName)
} else {
err = errors.NewConflict(r.resource, r.resourceName, err)
}
case http.StatusNotFound:
err = errors.NewNotFound(r.resource, r.resourceName)
case http.StatusBadRequest:
err = errors.NewBadRequest(message)
case http.StatusUnauthorized:
err = errors.NewUnauthorized(message)
case http.StatusForbidden:
err = errors.NewForbidden(r.resource, r.resourceName, err)
case errors.StatusUnprocessableEntity:
err = errors.NewInvalid(r.resource, r.resourceName, nil)
case errors.StatusServerTimeout:
retryAfterSeconds, _ := retryAfterSeconds(resp)
err = errors.NewServerTimeout(r.resource, r.verb, retryAfterSeconds)
case errors.StatusTooManyRequests:
retryAfterSeconds, _ := retryAfterSeconds(resp)
err = errors.NewServerTimeout(r.resource, r.verb, retryAfterSeconds)
case http.StatusInternalServerError:
err = errors.NewInternalError(fmt.Errorf(message))
}
return err
retryAfter, _ := retryAfterSeconds(resp)
return errors.NewGenericServerResponse(resp.StatusCode, req.Method, r.resource, r.resourceName, message, retryAfter)
}
// isTextResponse returns true if the response appears to be a textual media type.

View File

@@ -251,7 +251,7 @@ func TestTransformResponse(t *testing.T) {
},
Error: true,
ErrFn: func(err error) bool {
return err.Error() == "aaaaa" && apierrors.IsUnauthorized(err)
return strings.Contains(err.Error(), "server has asked for the client to provide") && apierrors.IsUnauthorized(err)
},
},
{Response: &http.Response{StatusCode: 403}, Error: true},