diff --git a/pkg/api/validation/schema.go b/pkg/api/validation/schema.go index 24c1e26199f..6c55bc47faa 100644 --- a/pkg/api/validation/schema.go +++ b/pkg/api/validation/schema.go @@ -44,6 +44,14 @@ func NewInvalidTypeError(expected reflect.Kind, observed reflect.Kind, fieldName return &InvalidTypeError{expected, observed, fieldName} } +// TypeNotFoundError is returned when specified type +// can not found in schema +type TypeNotFoundError string + +func (tnfe TypeNotFoundError) Error() string { + return fmt.Sprintf("couldn't find type: %s", string(tnfe)) +} + // Schema is an interface that knows how to validate an API object serialized to a byte array. type Schema interface { ValidateBytes(data []byte) error @@ -164,7 +172,7 @@ func (s *SwaggerSchema) ValidateObject(obj interface{}, fieldName, typeName stri models := s.api.Models model, ok := models.At(typeName) if !ok { - return append(allErrs, fmt.Errorf("couldn't find type: %s", typeName)) + return append(allErrs, TypeNotFoundError(typeName)) } properties := model.Properties if len(properties.List) == 0 { diff --git a/pkg/kubectl/cmd/util/factory.go b/pkg/kubectl/cmd/util/factory.go index 2e2258b0d96..9964160fdbf 100644 --- a/pkg/kubectl/cmd/util/factory.go +++ b/pkg/kubectl/cmd/util/factory.go @@ -729,6 +729,7 @@ func writeSchemaFile(schemaData []byte, cacheDir, cacheFile, prefix, groupVersio func getSchemaAndValidate(c schemaClient, data []byte, prefix, groupVersion, cacheDir string) (err error) { var schemaData []byte + var firstSeen bool fullDir, err := substituteUserHome(cacheDir) if err != nil { return err @@ -741,24 +742,50 @@ func getSchemaAndValidate(c schemaClient, data []byte, prefix, groupVersion, cac } } if schemaData == nil { - schemaData, err = c.Get(). - AbsPath("/swaggerapi", prefix, groupVersion). - Do(). - Raw() + firstSeen = true + schemaData, err = downloadSchemaAndStore(c, cacheDir, fullDir, cacheFile, prefix, groupVersion) if err != nil { return err } - if len(cacheDir) != 0 { - if err := writeSchemaFile(schemaData, fullDir, cacheFile, prefix, groupVersion); err != nil { - return err - } - } } schema, err := validation.NewSwaggerSchemaFromBytes(schemaData) if err != nil { return err } - return schema.ValidateBytes(data) + err = schema.ValidateBytes(data) + if _, ok := err.(validation.TypeNotFoundError); ok && !firstSeen { + // As a temporay hack, kubectl would re-get the schema if validation + // fails for type not found reason. + // TODO: runtime-config settings needs to make into the file's name + schemaData, err = downloadSchemaAndStore(c, cacheDir, fullDir, cacheFile, prefix, groupVersion) + if err != nil { + return err + } + schema, err := validation.NewSwaggerSchemaFromBytes(schemaData) + if err != nil { + return err + } + return schema.ValidateBytes(data) + } + + return err +} + +// Download swagger schema from apiserver and store it to file. +func downloadSchemaAndStore(c schemaClient, cacheDir, fullDir, cacheFile, prefix, groupVersion string) (schemaData []byte, err error) { + schemaData, err = c.Get(). + AbsPath("/swaggerapi", prefix, groupVersion). + Do(). + Raw() + if err != nil { + return + } + if len(cacheDir) != 0 { + if err = writeSchemaFile(schemaData, fullDir, cacheFile, prefix, groupVersion); err != nil { + return + } + } + return } func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { diff --git a/pkg/kubectl/cmd/util/factory_test.go b/pkg/kubectl/cmd/util/factory_test.go index be9484fae3c..03a4403d338 100644 --- a/pkg/kubectl/cmd/util/factory_test.go +++ b/pkg/kubectl/cmd/util/factory_test.go @@ -32,6 +32,7 @@ import ( "k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/validation" + "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" "k8s.io/kubernetes/pkg/client/unversioned/fake" @@ -215,6 +216,63 @@ func loadSchemaForTest() (validation.Schema, error) { return validation.NewSwaggerSchemaFromBytes(data) } +func TestRefetchSchemaWhenValidationFails(t *testing.T) { + schema, err := loadSchemaForTest() + if err != nil { + t.Errorf("Error loading schema: %v", err) + t.FailNow() + } + output, err := json.Marshal(schema) + if err != nil { + t.Errorf("Error serializing schema: %v", err) + t.FailNow() + } + requests := map[string]int{} + + c := &fake.RESTClient{ + Codec: testapi.Default.Codec(), + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case strings.HasPrefix(p, "/swaggerapi") && m == "GET": + requests[p] = requests[p] + 1 + return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBuffer(output))}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + dir := os.TempDir() + "/schemaCache" + os.RemoveAll(dir) + + fullDir, err := substituteUserHome(dir) + if err != nil { + t.Errorf("Error getting fullDir: %v", err) + t.FailNow() + } + cacheFile := path.Join(fullDir, "foo", "bar", schemaFileName) + err = writeSchemaFile(output, fullDir, cacheFile, "foo", "bar") + if err != nil { + t.Errorf("Error building old cache schema: %v", err) + t.FailNow() + } + + obj := &extensions.Deployment{} + data, err := runtime.Encode(testapi.Extensions.Codec(), obj) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + + // Re-get request, should use HTTP and write + if getSchemaAndValidate(c, data, "foo", "bar", dir); err != nil { + t.Errorf("unexpected error validating: %v", err) + } + if requests["/swaggerapi/foo/bar"] != 1 { + t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo/bar"]) + } +} + func TestValidateCachesSchema(t *testing.T) { schema, err := loadSchemaForTest() if err != nil { @@ -266,7 +324,7 @@ func TestValidateCachesSchema(t *testing.T) { if getSchemaAndValidate(c, data, "foo", "bar", dir); err != nil { t.Errorf("unexpected error validating: %v", err) } - if requests["/swaggerapi/foo/bar"] != 1 { + if requests["/swaggerapi/foo/bar"] != 2 { t.Errorf("expected 1 schema request, saw: %d", requests["/swaggerapi/foo/bar"]) }