1
2
3
4 package apply
5
6 import (
7 "bytes"
8 "context"
9 "fmt"
10 "io"
11 "net/http"
12 "regexp"
13 "testing"
14
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17 v1 "k8s.io/api/core/v1"
18 "k8s.io/apimachinery/pkg/api/meta"
19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
21 "k8s.io/apimachinery/pkg/runtime"
22 "k8s.io/apimachinery/pkg/runtime/schema"
23 "k8s.io/cli-runtime/pkg/resource"
24 dynamicfake "k8s.io/client-go/dynamic/fake"
25 "k8s.io/client-go/rest/fake"
26 clienttesting "k8s.io/client-go/testing"
27 "k8s.io/klog/v2"
28 cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
29 "k8s.io/kubectl/pkg/scheme"
30 "sigs.k8s.io/cli-utils/pkg/common"
31 "sigs.k8s.io/cli-utils/pkg/inventory"
32 "sigs.k8s.io/cli-utils/pkg/jsonpath"
33 pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
34 "sigs.k8s.io/cli-utils/pkg/kstatus/watcher"
35 "sigs.k8s.io/cli-utils/pkg/object"
36 )
37
38 type inventoryInfo struct {
39 name string
40 namespace string
41 id string
42 set object.ObjMetadataSet
43 }
44
45 func (i inventoryInfo) toUnstructured() *unstructured.Unstructured {
46 invMap := make(map[string]interface{})
47 for _, objMeta := range i.set {
48 invMap[objMeta.String()] = ""
49 }
50
51 return &unstructured.Unstructured{
52 Object: map[string]interface{}{
53 "apiVersion": "v1",
54 "kind": "ConfigMap",
55 "metadata": map[string]interface{}{
56 "name": i.name,
57 "namespace": i.namespace,
58 "labels": map[string]interface{}{
59 common.InventoryLabel: i.id,
60 },
61 },
62 "data": invMap,
63 },
64 }
65 }
66
67 func (i inventoryInfo) toWrapped() inventory.Info {
68 return inventory.WrapInventoryInfoObj(i.toUnstructured())
69 }
70
71 func newTestApplier(
72 t *testing.T,
73 invInfo inventoryInfo,
74 resources object.UnstructuredSet,
75 clusterObjs object.UnstructuredSet,
76 statusWatcher watcher.StatusWatcher,
77 ) *Applier {
78 tf := newTestFactory(t, invInfo, resources, clusterObjs)
79 defer tf.Cleanup()
80
81 infoHelper := &fakeInfoHelper{
82 factory: tf,
83 }
84
85 invClient := newTestInventory(t, tf)
86
87 applier, err := NewApplierBuilder().
88 WithFactory(tf).
89 WithInventoryClient(invClient).
90 WithStatusWatcher(statusWatcher).
91 Build()
92 require.NoError(t, err)
93
94
95
96 applier.infoHelper = infoHelper
97
98 return applier
99 }
100
101 func newTestDestroyer(
102 t *testing.T,
103 invInfo inventoryInfo,
104 clusterObjs object.UnstructuredSet,
105 statusWatcher watcher.StatusWatcher,
106 ) *Destroyer {
107 tf := newTestFactory(t, invInfo, object.UnstructuredSet{}, clusterObjs)
108 defer tf.Cleanup()
109
110 invClient := newTestInventory(t, tf)
111
112 destroyer, err := NewDestroyerBuilder().
113 WithFactory(tf).
114 WithInventoryClient(invClient).
115 Build()
116 require.NoError(t, err)
117 destroyer.statusWatcher = statusWatcher
118
119 return destroyer
120 }
121
122 func newTestInventory(
123 t *testing.T,
124 tf *cmdtesting.TestFactory,
125 ) inventory.Client {
126
127
128 invClient, err := inventory.ClusterClientFactory{StatusPolicy: inventory.StatusPolicyAll}.NewClient(tf)
129 require.NoError(t, err)
130 return invClient
131 }
132
133 func newTestFactory(
134 t *testing.T,
135 invInfo inventoryInfo,
136 resourceSet object.UnstructuredSet,
137 clusterObjs object.UnstructuredSet,
138 ) *cmdtesting.TestFactory {
139 tf := cmdtesting.NewTestFactory().WithNamespace(invInfo.namespace)
140
141 mapper, err := tf.ToRESTMapper()
142 require.NoError(t, err)
143
144 objMap := make(map[object.ObjMetadata]resourceInfo)
145 for _, r := range resourceSet {
146 objMeta := object.UnstructuredToObjMetadata(r)
147 objMap[objMeta] = resourceInfo{
148 resource: r,
149 exists: false,
150 }
151 }
152 for _, r := range clusterObjs {
153 objMeta := object.UnstructuredToObjMetadata(r)
154 objMap[objMeta] = resourceInfo{
155 resource: r,
156 exists: true,
157 }
158 }
159 var objs []resourceInfo
160 for _, obj := range objMap {
161 objs = append(objs, obj)
162 }
163
164 handlers := []handler{
165 &nsHandler{},
166 &genericHandler{
167 resources: objs,
168 mapper: mapper,
169 },
170 }
171
172 tf.UnstructuredClient = newFakeRESTClient(t, handlers)
173 tf.FakeDynamicClient = fakeDynamicClient(t, mapper, invInfo, objs...)
174
175 return tf
176 }
177
178 type resourceInfo struct {
179 resource *unstructured.Unstructured
180 exists bool
181 }
182
183
184
185
186
187
188 func newFakeRESTClient(t *testing.T, handlers []handler) *fake.RESTClient {
189 return &fake.RESTClient{
190 NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
191 Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
192 klog.V(5).Infof("FakeRESTClient: handling %s request for %q", req.Method, req.URL)
193 for _, h := range handlers {
194 resp, handled, err := h.handle(t, req)
195 if err != nil {
196 t.Fatalf("unexpected error: %v", err)
197 return nil, nil
198 }
199 if handled {
200 return resp, nil
201 }
202 }
203 t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
204 return nil, nil
205 }),
206 }
207 }
208
209
210
211
212
213 type handler interface {
214 handle(t *testing.T, req *http.Request) (*http.Response, bool, error)
215 }
216
217
218
219
220 type genericHandler struct {
221 resources []resourceInfo
222 mapper meta.RESTMapper
223 }
224
225 func (g *genericHandler) handle(t *testing.T, req *http.Request) (*http.Response, bool, error) {
226 klog.V(5).Infof("genericHandler: handling %s request for %q", req.Method, req.URL)
227 for _, r := range g.resources {
228 gvk := r.resource.GroupVersionKind()
229 mapping, err := g.mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
230 if err != nil {
231 return nil, false, err
232 }
233 var allPath string
234 if mapping.Scope == meta.RESTScopeNamespace {
235 allPath = fmt.Sprintf("/namespaces/%s/%s", r.resource.GetNamespace(), mapping.Resource.Resource)
236 } else {
237 allPath = fmt.Sprintf("/%s", mapping.Resource.Resource)
238 }
239 singlePath := allPath + "/" + r.resource.GetName()
240
241 if req.URL.Path == singlePath && req.Method == http.MethodGet {
242 if r.exists {
243 bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource)))
244 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil
245 }
246 return &http.Response{StatusCode: http.StatusNotFound, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.StringBody("")}, true, nil
247 }
248
249 if req.URL.Path == singlePath && req.Method == http.MethodPatch {
250 bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource)))
251 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil
252 }
253
254 if req.URL.Path == singlePath && req.Method == http.MethodDelete {
255 if r.exists {
256 bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource)))
257 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil
258 }
259
260
261
262
263 status := http.StatusOK
264
265
266 result := &metav1.Status{
267 Status: metav1.StatusSuccess,
268 Code: int32(status),
269 Details: &metav1.StatusDetails{
270 Name: r.resource.GetName(),
271 Kind: r.resource.GetKind(),
272 },
273 }
274 bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, result)))
275 return &http.Response{StatusCode: status, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil
276 }
277
278 if req.URL.Path == allPath && req.Method == http.MethodPost {
279 bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, r.resource)))
280 return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil
281 }
282 }
283 return nil, false, nil
284 }
285
286 func newInventoryReactor(invInfo inventoryInfo) *inventoryReactor {
287 return &inventoryReactor{
288 inventoryObj: invInfo.toUnstructured(),
289 }
290 }
291
292 type inventoryReactor struct {
293 inventoryObj *unstructured.Unstructured
294 }
295
296 func (ir *inventoryReactor) updateFakeDynamicClient(fdc *dynamicfake.FakeDynamicClient) {
297 fdc.PrependReactor("create", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) {
298 obj := *action.(clienttesting.CreateAction).GetObject().(*unstructured.Unstructured)
299 ir.inventoryObj = &obj
300 return true, ir.inventoryObj.DeepCopy(), nil
301 })
302 fdc.PrependReactor("list", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) {
303 uList := &unstructured.UnstructuredList{
304 Items: []unstructured.Unstructured{},
305 }
306 if ir.inventoryObj != nil {
307 uList.Items = append(uList.Items, *ir.inventoryObj.DeepCopy())
308 }
309 return true, uList, nil
310 })
311 fdc.PrependReactor("get", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) {
312 return true, ir.inventoryObj.DeepCopy(), nil
313 })
314 fdc.PrependReactor("update", "configmaps", func(action clienttesting.Action) (bool, runtime.Object, error) {
315 obj := *action.(clienttesting.UpdateAction).GetObject().(*unstructured.Unstructured)
316 ir.inventoryObj = &obj
317 return true, ir.inventoryObj.DeepCopy(), nil
318 })
319 }
320
321
322
323
324
325 type nsHandler struct{}
326
327 var (
328 nsPathRegex = regexp.MustCompile(`/api/v1/namespaces/([^/]+)`)
329 )
330
331 func (n *nsHandler) handle(t *testing.T, req *http.Request) (*http.Response, bool, error) {
332 match := nsPathRegex.FindStringSubmatch(req.URL.Path)
333 if req.Method == http.MethodGet && match != nil {
334 nsName := match[1]
335 ns := v1.Namespace{
336 TypeMeta: metav1.TypeMeta{
337 APIVersion: "v1",
338 Kind: "Namespace",
339 },
340 ObjectMeta: metav1.ObjectMeta{
341 Name: nsName,
342 },
343 }
344 bodyRC := io.NopCloser(bytes.NewReader(toJSONBytes(t, &ns)))
345 return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, true, nil
346 }
347 return nil, false, nil
348 }
349
350 type fakeWatcher struct {
351 start chan struct{}
352 events []pollevent.Event
353 }
354
355 func newFakeWatcher(statusEvents []pollevent.Event) *fakeWatcher {
356 return &fakeWatcher{
357 events: statusEvents,
358 start: make(chan struct{}),
359 }
360 }
361
362
363 func (f *fakeWatcher) Start() {
364 close(f.start)
365 }
366
367 func (f *fakeWatcher) Watch(ctx context.Context, _ object.ObjMetadataSet, _ watcher.Options) <-chan pollevent.Event {
368 eventChannel := make(chan pollevent.Event)
369 go func() {
370 defer close(eventChannel)
371
372 eventChannel <- pollevent.Event{Type: pollevent.SyncEvent}
373
374 <-f.start
375 for _, f := range f.events {
376 eventChannel <- f
377 }
378
379 <-ctx.Done()
380 }()
381 return eventChannel
382 }
383
384 type fakeInfoHelper struct {
385 factory *cmdtesting.TestFactory
386 }
387
388
389
390
391 func (f *fakeInfoHelper) UpdateInfo(info *resource.Info) error {
392 mapper, err := f.factory.ToRESTMapper()
393 if err != nil {
394 return err
395 }
396 gvk := info.Object.GetObjectKind().GroupVersionKind()
397 mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
398 if err != nil {
399 return err
400 }
401 info.Mapping = mapping
402
403 c, err := f.getClient(gvk.GroupVersion())
404 if err != nil {
405 return err
406 }
407 info.Client = c
408 return nil
409 }
410
411 func (f *fakeInfoHelper) BuildInfo(obj *unstructured.Unstructured) (*resource.Info, error) {
412 info := &resource.Info{
413 Name: obj.GetName(),
414 Namespace: obj.GetNamespace(),
415 Source: "unstructured",
416 Object: obj,
417 }
418 err := f.UpdateInfo(info)
419 return info, err
420 }
421
422 func (f *fakeInfoHelper) getClient(gv schema.GroupVersion) (resource.RESTClient, error) {
423 if f.factory.UnstructuredClientForMappingFunc != nil {
424 return f.factory.UnstructuredClientForMappingFunc(gv)
425 }
426 if f.factory.UnstructuredClient != nil {
427 return f.factory.UnstructuredClient, nil
428 }
429 return f.factory.Client, nil
430 }
431
432
433 func fakeDynamicClient(t *testing.T, mapper meta.RESTMapper, invInfo inventoryInfo, objs ...resourceInfo) *dynamicfake.FakeDynamicClient {
434 fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme)
435
436 invReactor := newInventoryReactor(invInfo)
437 invReactor.updateFakeDynamicClient(fakeClient)
438
439 for i := range objs {
440 obj := objs[i]
441 gvk := obj.resource.GroupVersionKind()
442 mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
443 if !assert.NoError(t, err) {
444 t.FailNow()
445 }
446 r := mapping.Resource.Resource
447 fakeClient.PrependReactor("get", r, func(clienttesting.Action) (bool, runtime.Object, error) {
448 if obj.exists {
449 return true, obj.resource, nil
450 }
451 return false, nil, nil
452 })
453 fakeClient.PrependReactor("delete", r, func(clienttesting.Action) (bool, runtime.Object, error) {
454 return true, nil, nil
455 })
456 }
457
458 return fakeClient
459 }
460
461 func toJSONBytes(t *testing.T, obj runtime.Object) []byte {
462 objBytes, err := runtime.Encode(unstructured.NewJSONFallbackEncoder(codec), obj)
463 if !assert.NoError(t, err) {
464 t.Fatal(err)
465 }
466 return objBytes
467 }
468
469 type JSONPathSetter struct {
470 Path string
471 Value interface{}
472 }
473
474 func (jps JSONPathSetter) Mutate(u *unstructured.Unstructured) {
475 _, err := jsonpath.Set(u.Object, jps.Path, jps.Value)
476 if err != nil {
477 panic(fmt.Sprintf("failed to mutate unstructured object: %v", err))
478 }
479 }
480
View as plain text