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