package api import ( "encoding/json" "fmt" "reflect" "regexp" "strconv" "strings" "testing" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util/validation/field" ) func TestCompatibility_v1_Pod(t *testing.T) { // Test "spec.serviceAccount" -> "spec.serviceAccountName" expectedServiceAccount := "my-service-account" input := []byte(fmt.Sprintf(` { "kind":"Pod", "apiVersion":"v1", "metadata":{"name":"my-pod-name", "namespace":"my-pod-namespace"}, "spec": { "serviceAccount":"%s", "containers":[{ "name":"my-container-name", "image":"my-container-image" }] } } `, expectedServiceAccount)) t.Log("Testing 1.0.0 v1 migration added in PR #3592") testCompatibility( t, "v1", input, func(obj runtime.Object) field.ErrorList { return validation.ValidatePod(obj.(*api.Pod)) }, map[string]string{ "spec.serviceAccount": expectedServiceAccount, "spec.serviceAccountName": expectedServiceAccount, }, ) } // TestCompatibility_v1_VolumeSource tests that the metadata field in volume // sources gets properly round-tripped func TestCompatibility_v1_VolumeSource(t *testing.T) { // Test volume source compatibility path := "test/volume/source/compat" metadata := []byte(fmt.Sprintf(` { "kind":"Pod", "apiVersion":"v1", "metadata":{"name":"my-pod-name", "namespace":"my-pod-namespace"}, "spec": { "containers":[{ "name":"my-container-name", "image":"my-container-image", "ports":[{"containerPort":1,"protocol":"TCP"}] }], "volumes":[{ "name":"a-metadata-volume", "metadata":{"items":[{"name":"%s","fieldRef":{"apiVersion":"v1","fieldPath":"metadata.name"}}]} }] } } `, path)) downward := []byte(fmt.Sprintf(` { "kind":"Pod", "apiVersion":"v1", "metadata":{"name":"my-pod-name", "namespace":"my-pod-namespace"}, "spec": { "containers":[{ "name":"my-container-name", "image":"my-container-image", "ports":[{"containerPort":1,"protocol":"TCP"}] }], "volumes":[{ "name":"a-downwardapi-volume", "downwardAPI":{"items":[{"path":"%s","fieldRef":{"apiVersion":"v1","fieldPath":"metadata.name"}}]} }] } } `, path)) t.Log("Testing 1.0.6 v1 migration added in PR #4663") testCompatibility( t, "v1", metadata, func(obj runtime.Object) field.ErrorList { return validation.ValidatePod(obj.(*api.Pod)) }, map[string]string{ "spec.volumes[0].metadata.items[0].name": path, "spec.volumes[0].downwardAPI.items[0].path": path, }, ) testCompatibility( t, "v1", downward, func(obj runtime.Object) field.ErrorList { return validation.ValidatePod(obj.(*api.Pod)) }, map[string]string{ "spec.volumes[0].metadata.items[0].name": path, "spec.volumes[0].downwardAPI.items[0].path": path, }, ) } func testCompatibility( t *testing.T, version string, input []byte, validator func(obj runtime.Object) field.ErrorList, serialized map[string]string, ) { // Decode obj, err := runtime.Decode(api.Codecs.UniversalDecoder(), input) if err != nil { t.Fatalf("Unexpected error: %v", err) } // Validate errs := validator(obj) if len(errs) != 0 { t.Fatalf("Unexpected errors: %v", errs) } // Encode output := runtime.EncodeOrDie(api.Codecs.LegacyCodec(unversioned.GroupVersion{Group: "", Version: version}), obj) // Validate old and new fields are encoded generic := map[string]interface{}{} if err := json.Unmarshal([]byte(output), &generic); err != nil { t.Fatalf("Unexpected error: %v", err) } for k, expectedValue := range serialized { keys := strings.Split(k, ".") if actualValue, ok, err := getJSONValue(generic, keys...); err != nil || !ok { t.Errorf("Unexpected error for %s: %v", k, err) } else if !reflect.DeepEqual(expectedValue, actualValue) { t.Errorf("Expected %v, got %v", expectedValue, actualValue) } } } func TestAllowedGrouplessVersion(t *testing.T) { versions := map[string]unversioned.GroupVersion{ "v1": {Group: "", Version: "v1"}, "v1beta3": {Group: "", Version: "v1beta3"}, "1.0": {Group: "", Version: "1.0"}, "pre012": {Group: "", Version: "pre012"}, } for apiVersion, expectedGroupVersion := range versions { groupVersion, err := unversioned.ParseGroupVersion(apiVersion) if err != nil { t.Errorf("%s: unexpected error parsing: %v", apiVersion, err) continue } if groupVersion != expectedGroupVersion { t.Errorf("%s: expected %#v, got %#v", apiVersion, expectedGroupVersion, groupVersion) continue } if groupVersion.String() != apiVersion { t.Errorf("%s: expected GroupVersion.String() to be %q, got %q", apiVersion, apiVersion, groupVersion.String()) continue } } } func getJSONValue(data map[string]interface{}, keys ...string) (interface{}, bool, error) { // No keys, current value is it if len(keys) == 0 { return data, true, nil } // Get the key (and optional index) key := keys[0] index := -1 if matches := regexp.MustCompile(`^(.*)\[(\d+)\]$`).FindStringSubmatch(key); len(matches) > 0 { key = matches[1] index, _ = strconv.Atoi(matches[2]) } // Look up the value value, ok := data[key] if !ok { return nil, false, fmt.Errorf("No key %s found", key) } // Get the indexed value if an index is specified if index >= 0 { valueSlice, ok := value.([]interface{}) if !ok { return nil, false, fmt.Errorf("Key %s did not hold a slice", key) } if index >= len(valueSlice) { return nil, false, fmt.Errorf("Index %d out of bounds for slice at key: %v", index, key) } value = valueSlice[index] } if len(keys) == 1 { return value, true, nil } childData, ok := value.(map[string]interface{}) if !ok { return nil, false, fmt.Errorf("Key %s did not hold a map", keys[0]) } return getJSONValue(childData, keys[1:]...) }