cli-utils/pkg/inventory/inventory-client_test.go

591 lines
16 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package inventory
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
"sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/object"
)
func TestGetClusterInventoryInfo(t *testing.T) {
tests := map[string]struct {
inv InventoryInfo
localObjs []object.ObjMetadata
isError bool
}{
"Nil local inventory object is an error": {
inv: nil,
localObjs: []object.ObjMetadata{},
isError: true,
},
"Empty local inventory object": {
inv: localInv,
localObjs: []object.ObjMetadata{},
isError: false,
},
"Local inventory with a single object": {
inv: localInv,
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod2Info),
},
isError: false,
},
"Local inventory with multiple objects": {
inv: localInv,
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod2Info),
ignoreErrInfoToObjMeta(pod3Info)},
isError: false,
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
fakeBuilder := FakeBuilder{}
fakeBuilder.SetInventoryObjs(tc.localObjs)
invClient.builderFunc = fakeBuilder.GetBuilder()
var inv *unstructured.Unstructured
if tc.inv != nil {
inv = storeObjsInInventory(tc.inv, tc.localObjs)
}
clusterInv, err := invClient.getClusterInventoryInfo(inv)
if tc.isError {
if err == nil {
t.Fatalf("expected error but received none")
}
return
}
if !tc.isError && err != nil {
t.Fatalf("unexpected error received: %s", err)
}
if clusterInv != nil {
wrapped := WrapInventoryObj(clusterInv)
clusterObjs, err := wrapped.Load()
if err != nil {
t.Fatalf("unexpected error received: %s", err)
}
if !object.SetEquals(tc.localObjs, clusterObjs) {
t.Fatalf("expected cluster objs (%v), got (%v)", tc.localObjs, clusterObjs)
}
}
})
}
}
func TestMerge(t *testing.T) {
tests := map[string]struct {
localInv InventoryInfo
localObjs []object.ObjMetadata
clusterObjs []object.ObjMetadata
pruneObjs []object.ObjMetadata
isError bool
}{
"Nil local inventory object is error": {
localInv: nil,
localObjs: []object.ObjMetadata{},
clusterObjs: []object.ObjMetadata{},
pruneObjs: []object.ObjMetadata{},
isError: true,
},
"Cluster and local inventories empty: no prune objects; no change": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{},
clusterObjs: []object.ObjMetadata{},
pruneObjs: []object.ObjMetadata{},
isError: false,
},
"Cluster and local inventories same: no prune objects; no change": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
},
clusterObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
},
pruneObjs: []object.ObjMetadata{},
isError: false,
},
"Cluster two obj, local one: prune obj": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
},
clusterObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod3Info),
},
pruneObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod3Info),
},
isError: false,
},
"Cluster multiple objs, local multiple different objs: prune objs": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod2Info),
},
clusterObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod2Info),
ignoreErrInfoToObjMeta(pod3Info)},
pruneObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod3Info),
},
isError: false,
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
for name, tc := range tests {
for i := range common.Strategies {
drs := common.Strategies[i]
t.Run(name, func(t *testing.T) {
// Create the local inventory object storing "tc.localObjs"
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
invClient.SetDryRunStrategy(drs)
// Create a fake builder to return "tc.clusterObjs" from
// the cluster inventory object.
fakeBuilder := FakeBuilder{}
fakeBuilder.SetInventoryObjs(tc.clusterObjs)
invClient.builderFunc = fakeBuilder.GetBuilder()
// Call "Merge" to create the union of clusterObjs and localObjs.
pruneObjs, err := invClient.Merge(tc.localInv, tc.localObjs)
if tc.isError {
if err == nil {
t.Fatalf("expected error but received none")
}
return
}
if !tc.isError && err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !object.SetEquals(tc.pruneObjs, pruneObjs) {
t.Errorf("expected (%v) prune objs; got (%v)", tc.pruneObjs, pruneObjs)
}
})
}
}
}
func TestCreateInventory(t *testing.T) {
tests := map[string]struct {
inv InventoryInfo
localObjs []object.ObjMetadata
isError bool
}{
"Nil local inventory object is an error": {
inv: nil,
localObjs: []object.ObjMetadata{},
isError: true,
},
"Empty local inventory object": {
inv: localInv,
localObjs: []object.ObjMetadata{},
isError: false,
},
"Local inventory with a single object": {
inv: localInv,
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod2Info),
},
isError: false,
},
"Local inventory with multiple objects": {
inv: localInv,
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod2Info),
ignoreErrInfoToObjMeta(pod3Info)},
isError: false,
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
// The fake client must see a POST to the confimap URL.
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "POST" && cmPathRegex.Match([]byte(req.URL.Path)) {
b, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
cm := corev1.ConfigMap{}
err = runtime.DecodeInto(codec, b, &cm)
if err != nil {
return nil, err
}
bodyRC := ioutil.NopCloser(bytes.NewReader(b))
return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
}
return nil, nil
}),
}
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
inv := invClient.invToUnstructuredFunc(tc.inv)
if inv != nil {
inv = storeObjsInInventory(tc.inv, tc.localObjs)
}
err := invClient.createInventoryObj(inv)
if !tc.isError && err != nil {
t.Fatalf("unexpected error received: %s", err)
}
if tc.isError && err == nil {
t.Fatalf("expected error but received none")
}
})
}
}
func TestReplace(t *testing.T) {
tests := map[string]struct {
localInv InventoryInfo
localObjs []object.ObjMetadata
clusterObjs []object.ObjMetadata
isError bool
}{
"Local inventory nil is error": {
localInv: nil,
localObjs: []object.ObjMetadata{},
clusterObjs: []object.ObjMetadata{},
isError: true,
},
"Cluster and local inventories empty: no error": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{},
clusterObjs: []object.ObjMetadata{},
isError: false,
},
"Cluster and local inventories same: no error": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
},
clusterObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
},
isError: false,
},
"Cluster two obj, local one: no error": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
},
clusterObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod3Info),
},
isError: false,
},
"Cluster multiple objs, local multiple different objs: no error": {
localInv: copyInventory(),
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod2Info),
},
clusterObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod2Info),
ignoreErrInfoToObjMeta(pod3Info)},
isError: false,
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
for name, tc := range tests {
for i := range common.Strategies {
drs := common.Strategies[i]
t.Run(name, func(t *testing.T) {
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
invClient.SetDryRunStrategy(drs)
// Create fake builder returning the cluster inventory object
// storing the "tc.clusterObjs" objects.
fakeBuilder := FakeBuilder{}
fakeBuilder.SetInventoryObjs(tc.clusterObjs)
invClient.builderFunc = fakeBuilder.GetBuilder()
// Call "Replace", passing in the new local inventory objects
err := invClient.Replace(tc.localInv, tc.localObjs)
if tc.isError {
if err == nil {
t.Fatalf("expected error but received none")
}
return
}
if !tc.isError && err != nil {
t.Fatalf("unexpected error received: %s", err)
}
})
}
}
}
func TestGetClusterObjs(t *testing.T) {
tests := map[string]struct {
localInv InventoryInfo
clusterObjs []object.ObjMetadata
isError bool
}{
"Nil cluster inventory is error": {
localInv: nil,
clusterObjs: []object.ObjMetadata{},
isError: true,
},
"No cluster objs": {
localInv: copyInventory(),
clusterObjs: []object.ObjMetadata{},
isError: false,
},
"Single cluster obj": {
localInv: copyInventory(),
clusterObjs: []object.ObjMetadata{ignoreErrInfoToObjMeta(pod1Info)},
isError: false,
},
"Multiple cluster objs": {
localInv: copyInventory(),
clusterObjs: []object.ObjMetadata{ignoreErrInfoToObjMeta(pod1Info), ignoreErrInfoToObjMeta(pod3Info)},
isError: false,
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
// Create fake builder returning "tc.clusterObjs" from cluster inventory.
fakeBuilder := FakeBuilder{}
fakeBuilder.SetInventoryObjs(tc.clusterObjs)
invClient.builderFunc = fakeBuilder.GetBuilder()
// Call "GetClusterObjs" and compare returned cluster inventory objs to expected.
clusterObjs, err := invClient.GetClusterObjs(tc.localInv)
if tc.isError {
if err == nil {
t.Fatalf("expected error but received none")
}
return
}
if !tc.isError && err != nil {
t.Fatalf("unexpected error received: %s", err)
}
if !object.SetEquals(tc.clusterObjs, clusterObjs) {
t.Errorf("expected (%v) cluster inventory objs; got (%v)", tc.clusterObjs, clusterObjs)
}
})
}
}
func TestDeleteInventoryObj(t *testing.T) {
tests := map[string]struct {
inv InventoryInfo
localObjs []object.ObjMetadata
}{
"Nil local inventory object is an error": {
inv: nil,
localObjs: []object.ObjMetadata{},
},
"Empty local inventory object": {
inv: localInv,
localObjs: []object.ObjMetadata{},
},
"Local inventory with a single object": {
inv: localInv,
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod2Info),
},
},
"Local inventory with multiple objects": {
inv: localInv,
localObjs: []object.ObjMetadata{
ignoreErrInfoToObjMeta(pod1Info),
ignoreErrInfoToObjMeta(pod2Info),
ignoreErrInfoToObjMeta(pod3Info)},
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
tf.UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
if req.Method == "DELETE" && cmPathRegex.Match([]byte(req.URL.Path)) {
b, err := ioutil.ReadAll(req.Body)
if err != nil {
return nil, err
}
cm := corev1.ConfigMap{}
err = runtime.DecodeInto(codec, b, &cm)
if err != nil {
return nil, err
}
bodyRC := ioutil.NopCloser(bytes.NewReader(b))
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: bodyRC}, nil
}
return nil, nil
}),
}
tf.ClientConfigVal = cmdtesting.DefaultClientConfig()
for name, tc := range tests {
for i := range common.Strategies {
drs := common.Strategies[i]
t.Run(name, func(t *testing.T) {
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
invClient.SetDryRunStrategy(drs)
inv := invClient.invToUnstructuredFunc(tc.inv)
if inv != nil {
inv = storeObjsInInventory(tc.inv, tc.localObjs)
}
err := invClient.deleteInventoryObj(inv)
if err != nil {
t.Fatalf("unexpected error received: %s", err)
}
})
}
}
}
type invAndObjs struct {
inv InventoryInfo
invObjs []object.ObjMetadata
}
func TestMergeInventoryObjs(t *testing.T) {
pod1Obj := ignoreErrInfoToObjMeta(pod1Info)
pod2Obj := ignoreErrInfoToObjMeta(pod2Info)
pod3Obj := ignoreErrInfoToObjMeta(pod3Info)
tests := map[string]struct {
invs []invAndObjs
expected []object.ObjMetadata
}{
"Single inventory object with no inventory is valid": {
invs: []invAndObjs{
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{},
},
},
expected: []object.ObjMetadata{},
},
"Single inventory object returns same objects": {
invs: []invAndObjs{
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod1Obj},
},
},
expected: []object.ObjMetadata{pod1Obj},
},
"Two inventories with the same objects returns them": {
invs: []invAndObjs{
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod1Obj},
},
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod1Obj},
},
},
expected: []object.ObjMetadata{pod1Obj},
},
"Two inventories with different retain the union": {
invs: []invAndObjs{
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod1Obj},
},
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod2Obj},
},
},
expected: []object.ObjMetadata{pod1Obj, pod2Obj},
},
"More than two inventory objects retains all objects": {
invs: []invAndObjs{
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod1Obj, pod2Obj},
},
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod2Obj},
},
{
inv: copyInventory(),
invObjs: []object.ObjMetadata{pod3Obj},
},
},
expected: []object.ObjMetadata{pod1Obj, pod2Obj, pod3Obj},
},
}
tf := cmdtesting.NewTestFactory().WithNamespace(testNamespace)
defer tf.Cleanup()
for name, tc := range tests {
for i := range common.Strategies {
drs := common.Strategies[i]
t.Run(name, func(t *testing.T) {
invClient, _ := NewInventoryClient(tf,
WrapInventoryObj, InvInfoToConfigMap)
invClient.SetDryRunStrategy(drs)
inventories := []*unstructured.Unstructured{}
for _, i := range tc.invs {
inv := storeObjsInInventory(i.inv, i.invObjs)
inventories = append(inventories, inv)
}
retained, err := invClient.mergeClusterInventory(inventories)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
wrapped := WrapInventoryObj(retained)
mergedObjs, _ := wrapped.Load()
if !object.SetEquals(tc.expected, mergedObjs) {
t.Errorf("expected merged inventory objects (%v), got (%v)", tc.expected, mergedObjs)
}
})
}
}
}
func ignoreErrInfoToObjMeta(info *resource.Info) object.ObjMetadata {
objMeta, _ := object.InfoToObjMeta(info)
return objMeta
}