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 util 18 19import ( 20 "fmt" 21 "io/ioutil" 22 "net/http" 23 "os" 24 "strings" 25 "syscall" 26 "testing" 27 28 corev1 "k8s.io/api/core/v1" 29 apiequality "k8s.io/apimachinery/pkg/api/equality" 30 "k8s.io/apimachinery/pkg/api/errors" 31 "k8s.io/apimachinery/pkg/api/meta" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/apimachinery/pkg/util/diff" 36 "k8s.io/apimachinery/pkg/util/validation/field" 37 "k8s.io/kubectl/pkg/scheme" 38 "k8s.io/utils/exec" 39) 40 41func TestMerge(t *testing.T) { 42 tests := []struct { 43 obj runtime.Object 44 fragment string 45 expected runtime.Object 46 expectErr bool 47 }{ 48 { 49 obj: &corev1.Pod{ 50 ObjectMeta: metav1.ObjectMeta{ 51 Name: "foo", 52 }, 53 }, 54 fragment: fmt.Sprintf(`{ "apiVersion": "%s" }`, "v1"), 55 expected: &corev1.Pod{ 56 TypeMeta: metav1.TypeMeta{ 57 Kind: "Pod", 58 APIVersion: "v1", 59 }, 60 ObjectMeta: metav1.ObjectMeta{ 61 Name: "foo", 62 }, 63 Spec: corev1.PodSpec{}, 64 }, 65 }, 66 /* TODO: uncomment this test once Merge is updated to use 67 strategic-merge-patch. See #8449. 68 { 69 obj: &corev1.Pod{ 70 ObjectMeta: metav1.ObjectMeta{ 71 Name: "foo", 72 }, 73 Spec: corev1.PodSpec{ 74 Containers: []corev1.Container{ 75 corev1.Container{ 76 Name: "c1", 77 Image: "red-image", 78 }, 79 corev1.Container{ 80 Name: "c2", 81 Image: "blue-image", 82 }, 83 }, 84 }, 85 }, 86 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "containers": [ { "name": "c1", "image": "green-image" } ] } }`, schema.GroupVersion{Group:"", Version: "v1"}.String()), 87 expected: &corev1.Pod{ 88 ObjectMeta: metav1.ObjectMeta{ 89 Name: "foo", 90 }, 91 Spec: corev1.PodSpec{ 92 Containers: []corev1.Container{ 93 corev1.Container{ 94 Name: "c1", 95 Image: "green-image", 96 }, 97 corev1.Container{ 98 Name: "c2", 99 Image: "blue-image", 100 }, 101 }, 102 }, 103 }, 104 }, */ 105 { 106 obj: &corev1.Pod{ 107 ObjectMeta: metav1.ObjectMeta{ 108 Name: "foo", 109 }, 110 }, 111 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "volumes": [ {"name": "v1"}, {"name": "v2"} ] } }`, "v1"), 112 expected: &corev1.Pod{ 113 TypeMeta: metav1.TypeMeta{ 114 Kind: "Pod", 115 APIVersion: "v1", 116 }, 117 ObjectMeta: metav1.ObjectMeta{ 118 Name: "foo", 119 }, 120 Spec: corev1.PodSpec{ 121 Volumes: []corev1.Volume{ 122 { 123 Name: "v1", 124 }, 125 { 126 Name: "v2", 127 }, 128 }, 129 }, 130 }, 131 }, 132 { 133 obj: &corev1.Pod{}, 134 fragment: "invalid json", 135 expected: &corev1.Pod{}, 136 expectErr: true, 137 }, 138 { 139 obj: &corev1.Service{}, 140 fragment: `{ "apiVersion": "badVersion" }`, 141 expectErr: true, 142 }, 143 { 144 obj: &corev1.Service{ 145 Spec: corev1.ServiceSpec{}, 146 }, 147 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "ports": [ { "port": 0 } ] } }`, "v1"), 148 expected: &corev1.Service{ 149 TypeMeta: metav1.TypeMeta{ 150 Kind: "Service", 151 APIVersion: "v1", 152 }, 153 Spec: corev1.ServiceSpec{ 154 Ports: []corev1.ServicePort{ 155 { 156 Port: 0, 157 }, 158 }, 159 }, 160 }, 161 }, 162 { 163 obj: &corev1.Service{ 164 Spec: corev1.ServiceSpec{ 165 Selector: map[string]string{ 166 "version": "v1", 167 }, 168 }, 169 }, 170 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "selector": { "version": "v2" } } }`, "v1"), 171 expected: &corev1.Service{ 172 TypeMeta: metav1.TypeMeta{ 173 Kind: "Service", 174 APIVersion: "v1", 175 }, 176 Spec: corev1.ServiceSpec{ 177 Selector: map[string]string{ 178 "version": "v2", 179 }, 180 }, 181 }, 182 }, 183 } 184 185 codec := runtime.NewCodec(scheme.DefaultJSONEncoder(), 186 scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...)) 187 for i, test := range tests { 188 out, err := Merge(codec, test.obj, test.fragment) 189 if !test.expectErr { 190 if err != nil { 191 t.Errorf("testcase[%d], unexpected error: %v", i, err) 192 } else if !apiequality.Semantic.DeepEqual(test.expected, out) { 193 t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, diff.ObjectReflectDiff(test.expected, out)) 194 } 195 } 196 if test.expectErr && err == nil { 197 t.Errorf("testcase[%d], unexpected non-error", i) 198 } 199 } 200} 201 202type checkErrTestCase struct { 203 err error 204 expectedErr string 205 expectedCode int 206} 207 208func TestCheckInvalidErr(t *testing.T) { 209 testCheckError(t, []checkErrTestCase{ 210 { 211 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid1").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field"), "single", "details")}), 212 "The Invalid1 \"invalidation\" is invalid: field: Invalid value: \"single\": details\n", 213 DefaultErrorExitCode, 214 }, 215 { 216 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid2").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field1"), "multi1", "details"), field.Invalid(field.NewPath("field2"), "multi2", "details")}), 217 "The Invalid2 \"invalidation\" is invalid: \n* field1: Invalid value: \"multi1\": details\n* field2: Invalid value: \"multi2\": details\n", 218 DefaultErrorExitCode, 219 }, 220 { 221 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid3").GroupKind(), "invalidation", field.ErrorList{}), 222 "The Invalid3 \"invalidation\" is invalid", 223 DefaultErrorExitCode, 224 }, 225 { 226 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid4").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field4"), "multi4", "details"), field.Invalid(field.NewPath("field4"), "multi4", "details")}), 227 "The Invalid4 \"invalidation\" is invalid: field4: Invalid value: \"multi4\": details\n", 228 DefaultErrorExitCode, 229 }, 230 { 231 &errors.StatusError{metav1.Status{ 232 Status: metav1.StatusFailure, 233 Code: http.StatusUnprocessableEntity, 234 Reason: metav1.StatusReasonInvalid, 235 // Details is nil. 236 }}, 237 "The request is invalid", 238 DefaultErrorExitCode, 239 }, 240 }) 241} 242 243func TestCheckNoResourceMatchError(t *testing.T) { 244 testCheckError(t, []checkErrTestCase{ 245 { 246 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Resource: "foo"}}, 247 `the server doesn't have a resource type "foo"`, 248 DefaultErrorExitCode, 249 }, 250 { 251 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Version: "theversion", Resource: "foo"}}, 252 `the server doesn't have a resource type "foo" in version "theversion"`, 253 DefaultErrorExitCode, 254 }, 255 { 256 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Version: "theversion", Resource: "foo"}}, 257 `the server doesn't have a resource type "foo" in group "thegroup" and version "theversion"`, 258 DefaultErrorExitCode, 259 }, 260 { 261 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Resource: "foo"}}, 262 `the server doesn't have a resource type "foo" in group "thegroup"`, 263 DefaultErrorExitCode, 264 }, 265 }) 266} 267 268func TestCheckExitError(t *testing.T) { 269 testCheckError(t, []checkErrTestCase{ 270 { 271 exec.CodeExitError{Err: fmt.Errorf("pod foo/bar terminated"), Code: 42}, 272 "pod foo/bar terminated", 273 42, 274 }, 275 }) 276} 277 278func testCheckError(t *testing.T, tests []checkErrTestCase) { 279 var errReturned string 280 var codeReturned int 281 errHandle := func(err string, code int) { 282 errReturned = err 283 codeReturned = code 284 } 285 286 for _, test := range tests { 287 checkErr(test.err, errHandle) 288 289 if errReturned != test.expectedErr { 290 t.Fatalf("Got: %s, expected: %s", errReturned, test.expectedErr) 291 } 292 if codeReturned != test.expectedCode { 293 t.Fatalf("Got: %d, expected: %d", codeReturned, test.expectedCode) 294 } 295 } 296} 297 298func TestDumpReaderToFile(t *testing.T) { 299 testString := "TEST STRING" 300 tempFile, err := ioutil.TempFile(os.TempDir(), "hlpers_test_dump_") 301 if err != nil { 302 t.Errorf("unexpected error setting up a temporary file %v", err) 303 } 304 defer syscall.Unlink(tempFile.Name()) 305 defer tempFile.Close() 306 defer func() { 307 if !t.Failed() { 308 os.Remove(tempFile.Name()) 309 } 310 }() 311 err = DumpReaderToFile(strings.NewReader(testString), tempFile.Name()) 312 if err != nil { 313 t.Errorf("error in DumpReaderToFile: %v", err) 314 } 315 data, err := ioutil.ReadFile(tempFile.Name()) 316 if err != nil { 317 t.Errorf("error when reading %s: %v", tempFile.Name(), err) 318 } 319 stringData := string(data) 320 if stringData != testString { 321 t.Fatalf("Wrong file content %s != %s", testString, stringData) 322 } 323} 324