github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/commands/live/migrate/migratecmd_test.go (about)

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package migrate
    16  
    17  import (
    18  	"os"
    19  	"path/filepath"
    20  	"strings"
    21  	"testing"
    22  
    23  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    24  	"github.com/GoogleContainerTools/kpt/internal/printer/fake"
    25  	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
    26  	"github.com/stretchr/testify/assert"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/cli-runtime/pkg/genericclioptions"
    29  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    30  	"k8s.io/kubectl/pkg/cmd/util"
    31  	"sigs.k8s.io/cli-utils/pkg/common"
    32  	"sigs.k8s.io/cli-utils/pkg/inventory"
    33  	"sigs.k8s.io/cli-utils/pkg/manifestreader"
    34  	"sigs.k8s.io/cli-utils/pkg/object"
    35  	"sigs.k8s.io/kustomize/kyaml/filesys"
    36  )
    37  
    38  var testNamespace = "test-inventory-namespace"
    39  var inventoryObjName = "test-inventory-obj"
    40  var testInventoryLabel = "test-inventory-label"
    41  
    42  var rgInvObj = &unstructured.Unstructured{
    43  	Object: map[string]interface{}{
    44  		"apiVersion": "kpt.dev/v1alpha1",
    45  		"kind":       "ResourceGroup",
    46  		"metadata": map[string]interface{}{
    47  			"name":      inventoryObjName,
    48  			"namespace": testNamespace,
    49  			"labels": map[string]interface{}{
    50  				common.InventoryLabel: testInventoryLabel,
    51  			},
    52  		},
    53  		"spec": map[string]interface{}{
    54  			"resources": []interface{}{},
    55  		},
    56  	},
    57  }
    58  
    59  var cmInvStr = `
    60  kind: ConfigMap
    61  apiVersion: v1
    62  metadata:
    63    name:      test-inventory-obj
    64    namespace: test-inventory-namespace
    65    labels:
    66      cli-utils.sigs.k8s.io/inventory-id: test-inventory-label
    67  `
    68  
    69  var cmInvObj = &unstructured.Unstructured{
    70  	Object: map[string]interface{}{
    71  		"apiVersion": "v1",
    72  		"kind":       "ConfigMap",
    73  		"metadata": map[string]interface{}{
    74  			"name":      inventoryObjName,
    75  			"namespace": testNamespace,
    76  			"labels": map[string]interface{}{
    77  				common.InventoryLabel: testInventoryLabel,
    78  			},
    79  		},
    80  	},
    81  }
    82  
    83  var pod1 = &unstructured.Unstructured{
    84  	Object: map[string]interface{}{
    85  		"apiVersion": "v1",
    86  		"kind":       "Pod",
    87  		"metadata": map[string]interface{}{
    88  			"name":      "pod-1",
    89  			"namespace": testNamespace,
    90  		},
    91  	},
    92  }
    93  
    94  var pod2 = &unstructured.Unstructured{
    95  	Object: map[string]interface{}{
    96  		"apiVersion": "v1",
    97  		"kind":       "Pod",
    98  		"metadata": map[string]interface{}{
    99  			"name":      "pod-2",
   100  			"namespace": testNamespace,
   101  		},
   102  	},
   103  }
   104  
   105  func TestKptMigrate_migrateKptfileToRG(t *testing.T) {
   106  	testCases := map[string]struct {
   107  		kptfile       string
   108  		rgFilename    string
   109  		resourcegroup string
   110  		dryRun        bool
   111  		isError       bool
   112  	}{
   113  		"Missing Kptfile is an error": {
   114  			kptfile:    "",
   115  			rgFilename: "resourcegroup.yaml",
   116  			dryRun:     false,
   117  			isError:    true,
   118  		},
   119  		"Kptfile with existing inventory will create ResourceGroup": {
   120  			kptfile:    kptFileWithInventory,
   121  			rgFilename: "resourcegroup.yaml",
   122  			dryRun:     false,
   123  			isError:    false,
   124  		},
   125  		"ResopurceGroup file already exists will error": {
   126  			kptfile:       kptFileWithInventory,
   127  			rgFilename:    "resourcegroup.yaml",
   128  			resourcegroup: resourceGroupInventory,
   129  			dryRun:        false,
   130  			isError:       true,
   131  		},
   132  		"Dry-run will not fill in inventory fields": {
   133  			kptfile:    kptFile,
   134  			rgFilename: "resourcegroup.yaml",
   135  			dryRun:     true,
   136  			isError:    false,
   137  		},
   138  		"Custom ResourceGroup file will be generated": {
   139  			kptfile:    kptFileWithInventory,
   140  			rgFilename: "custom-rg.yaml",
   141  			dryRun:     false,
   142  			isError:    false,
   143  		},
   144  	}
   145  
   146  	for tn, tc := range testCases {
   147  		t.Run(tn, func(t *testing.T) {
   148  			// Set up fake test factory
   149  			tf := cmdtesting.NewTestFactory().WithNamespace(inventoryNamespace)
   150  			defer tf.Cleanup()
   151  			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   152  
   153  			// Set up temp directory with Ktpfile
   154  			dir := t.TempDir()
   155  			p := filepath.Join(dir, "Kptfile")
   156  			err := os.WriteFile(p, []byte(tc.kptfile), 0600)
   157  			assert.NoError(t, err)
   158  
   159  			if tc.resourcegroup != "" {
   160  				p := filepath.Join(dir, tc.rgFilename)
   161  				err = os.WriteFile(p, []byte(tc.resourcegroup), 0600)
   162  				assert.NoError(t, err)
   163  			}
   164  
   165  			ctx := fake.CtxWithDefaultPrinter()
   166  			// Create MigrateRunner and call "updateKptfile"
   167  			cmLoader := manifestreader.NewManifestLoader(tf)
   168  			migrateRunner := NewRunner(ctx, tf, cmLoader, ioStreams)
   169  			migrateRunner.dryRun = tc.dryRun
   170  			migrateRunner.rgFile = tc.rgFilename
   171  			migrateRunner.cmInvClientFunc = func(factory util.Factory) (inventory.Client, error) {
   172  				return inventory.NewFakeClient([]object.ObjMetadata{}), nil
   173  			}
   174  			err = migrateRunner.migrateKptfileToRG([]string{dir})
   175  			// Check if there should be an error
   176  			if tc.isError {
   177  				if err == nil {
   178  					t.Fatalf("expected error but received none")
   179  				}
   180  				return
   181  			}
   182  			assert.NoError(t, err)
   183  			kf, err := pkg.ReadKptfile(filesys.FileSystemOrOnDisk{}, dir)
   184  			if !assert.NoError(t, err) {
   185  				t.FailNow()
   186  			}
   187  
   188  			rg, err := pkg.ReadRGFile(dir, migrateRunner.rgFile)
   189  			if !tc.dryRun && !assert.NoError(t, err) {
   190  				t.FailNow()
   191  			}
   192  
   193  			// Ensure the Kptfile does not contain inventory information.
   194  			if !assert.Nil(t, kf.Inventory) {
   195  				t.Errorf("inventory information should not be set in Kptfile")
   196  			}
   197  
   198  			if !tc.dryRun {
   199  				if rg == nil {
   200  					t.Fatalf("unable to read ResourceGroup file")
   201  				}
   202  				assert.Equal(t, inventoryNamespace, rg.ObjectMeta.Namespace)
   203  				if len(rg.ObjectMeta.Name) == 0 {
   204  					t.Errorf("inventory name not set in Kptfile")
   205  				}
   206  				if rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel] != testInventoryID {
   207  					t.Errorf("inventory id not set correctly in ResourceGroup: %s", rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel])
   208  				}
   209  			} else if rg != nil {
   210  				t.Errorf("inventory shouldn't be set during dryrun")
   211  			}
   212  		})
   213  	}
   214  }
   215  
   216  func TestKptMigrate_retrieveConfigMapInv(t *testing.T) {
   217  	testCases := map[string]struct {
   218  		configMap string
   219  		expected  *unstructured.Unstructured
   220  		isError   bool
   221  	}{
   222  		"Missing ConfigMap is an error": {
   223  			configMap: "",
   224  			expected:  nil,
   225  			isError:   true,
   226  		},
   227  		"ConfigMap inventory object is correctly retrieved": {
   228  			configMap: cmInvStr,
   229  			expected:  cmInvObj,
   230  			isError:   false,
   231  		},
   232  	}
   233  
   234  	for tn, tc := range testCases {
   235  		t.Run(tn, func(t *testing.T) {
   236  			// Set up fake test factory
   237  			tf := cmdtesting.NewTestFactory().WithNamespace(inventoryNamespace)
   238  			defer tf.Cleanup()
   239  			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   240  
   241  			ctx := fake.CtxWithDefaultPrinter()
   242  			// Create MigrateRunner and call "retrieveConfigMapInv"
   243  			cmLoader := manifestreader.NewManifestLoader(tf)
   244  			migrateRunner := NewRunner(ctx, tf, cmLoader, ioStreams)
   245  			migrateRunner.cmInvClientFunc = func(factory util.Factory) (inventory.Client, error) {
   246  				return inventory.NewFakeClient([]object.ObjMetadata{}), nil
   247  			}
   248  			actual, err := migrateRunner.retrieveConfigMapInv(strings.NewReader(tc.configMap), []string{"-"})
   249  			// Check if there should be an error
   250  			if tc.isError {
   251  				if err == nil {
   252  					t.Fatalf("expected error but received none")
   253  				}
   254  				return
   255  			}
   256  			assert.NoError(t, err)
   257  			if tc.expected.GetName() != actual.Name() {
   258  				t.Errorf("expected ConfigMap (%#v), got (%#v)", tc.expected, actual)
   259  			}
   260  			if tc.expected.GetNamespace() != actual.Namespace() {
   261  				t.Errorf("expected ConfigMap (%#v), got (%#v)", tc.expected, actual)
   262  			}
   263  		})
   264  	}
   265  }
   266  
   267  func TestKptMigrate_findResourceGroupInv(t *testing.T) {
   268  	testCases := map[string]struct {
   269  		objs     []*unstructured.Unstructured
   270  		expected *unstructured.Unstructured
   271  		isError  bool
   272  	}{
   273  		"Empty objs returns an error": {
   274  			objs:     []*unstructured.Unstructured{},
   275  			expected: nil,
   276  			isError:  true,
   277  		},
   278  		"Objs without inventory obj returns an error": {
   279  			objs:     []*unstructured.Unstructured{pod1},
   280  			expected: nil,
   281  			isError:  true,
   282  		},
   283  		"Objs without ConfigMap inventory obj returns an error": {
   284  			objs:     []*unstructured.Unstructured{cmInvObj, pod1},
   285  			expected: nil,
   286  			isError:  true,
   287  		},
   288  		"Objs without ResourceGroup inventory obj returns ResourceGroup": {
   289  			objs:     []*unstructured.Unstructured{rgInvObj, pod1},
   290  			expected: rgInvObj,
   291  			isError:  false,
   292  		},
   293  	}
   294  
   295  	for tn, tc := range testCases {
   296  		t.Run(tn, func(t *testing.T) {
   297  			actual, err := findResourceGroupInv(tc.objs)
   298  			if tc.isError {
   299  				if err == nil {
   300  					t.Fatalf("expected error but received none")
   301  				}
   302  				return
   303  			}
   304  			assert.NoError(t, err)
   305  			if tc.expected != actual {
   306  				t.Errorf("expected ResourceGroup (%#v), got (%#v)", tc.expected, actual)
   307  			}
   308  		})
   309  	}
   310  }
   311  
   312  func TestKptMigrate_migrateObjs(t *testing.T) {
   313  	testCases := map[string]struct {
   314  		invObj     string
   315  		objs       []object.ObjMetadata
   316  		isError    bool
   317  		rgFilename string
   318  	}{
   319  		"No objects to migrate is valid": {
   320  			invObj:  "",
   321  			objs:    []object.ObjMetadata{},
   322  			isError: false,
   323  		},
   324  		"One migrate object is valid": {
   325  			invObj:     resourceGroupInventory,
   326  			objs:       []object.ObjMetadata{object.UnstructuredToObjMetadata(pod1)},
   327  			rgFilename: rgfilev1alpha1.RGFileName,
   328  			isError:    false,
   329  		},
   330  		"Multiple migrate objects are valid": {
   331  			invObj: resourceGroupInventory,
   332  			objs: []object.ObjMetadata{
   333  				object.UnstructuredToObjMetadata(pod1),
   334  				object.UnstructuredToObjMetadata(pod2),
   335  			},
   336  			rgFilename: rgfilev1alpha1.RGFileName,
   337  			isError:    false,
   338  		},
   339  		"Kptfile does not have inventory is valid": {
   340  			invObj:     resourceGroupInventory,
   341  			objs:       []object.ObjMetadata{},
   342  			rgFilename: rgfilev1alpha1.RGFileName,
   343  			isError:    false,
   344  		},
   345  		"One migrate object is valid with inventory in Kptfile": {
   346  			invObj:     resourceGroupInventory,
   347  			objs:       []object.ObjMetadata{object.UnstructuredToObjMetadata(pod1)},
   348  			rgFilename: rgfilev1alpha1.RGFileName,
   349  			isError:    false,
   350  		},
   351  		"Migrate to ResourceGroup object with custom filename": {
   352  			invObj:     resourceGroupInventory,
   353  			objs:       []object.ObjMetadata{object.UnstructuredToObjMetadata(pod1)},
   354  			rgFilename: "test-rg.yaml",
   355  			isError:    false,
   356  		},
   357  	}
   358  
   359  	for tn, tc := range testCases {
   360  		t.Run(tn, func(t *testing.T) {
   361  			// Set up fake test factory
   362  			tf := cmdtesting.NewTestFactory().WithNamespace(inventoryNamespace)
   363  			defer tf.Cleanup()
   364  			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
   365  
   366  			ctx := fake.CtxWithDefaultPrinter()
   367  			// Create MigrateRunner and call "retrieveConfigMapInv"
   368  			rgInvClient := inventory.NewFakeClient(tc.objs)
   369  			cmLoader := manifestreader.NewManifestLoader(tf)
   370  			migrateRunner := NewRunner(ctx, tf, cmLoader, ioStreams)
   371  			migrateRunner.rgFile = tc.rgFilename
   372  
   373  			err := migrateRunner.migrateObjs(rgInvClient, tc.objs, strings.NewReader(tc.invObj), []string{"-"})
   374  			// Check if there should be an error
   375  			if tc.isError {
   376  				if err == nil {
   377  					t.Fatalf("expected error but received none")
   378  				}
   379  				return
   380  			}
   381  			assert.NoError(t, err)
   382  			// Retrieve the objects stored by the inventory client and validate.
   383  			migratedObjs, err := rgInvClient.GetClusterObjs(nil)
   384  			assert.NoError(t, err)
   385  			if len(tc.objs) != len(migratedObjs) {
   386  				t.Errorf("expected num migrated objs (%d), got (%d)", len(tc.objs), len(migratedObjs))
   387  			}
   388  			for _, migratedObj := range migratedObjs {
   389  				found := false
   390  				var expectedObj object.ObjMetadata
   391  				for _, expectedObj = range tc.objs {
   392  					if expectedObj == migratedObj {
   393  						found = true
   394  					}
   395  				}
   396  				if !found {
   397  					t.Fatalf("expected migrated object (%#v), but not found", expectedObj)
   398  					return
   399  				}
   400  			}
   401  		})
   402  	}
   403  }
   404  
   405  var kptFileWithInventory = `
   406  apiVersion: kpt.dev/v1
   407  kind: Kptfile
   408  metadata:
   409    name: test1
   410  upstreamLock:
   411    type: git
   412    git:
   413      repo: git@github.com:seans3/blueprint-helloworld
   414      directory: /
   415      ref: master
   416  inventory:
   417      name: foo
   418      namespace: test-namespace
   419      inventoryID: ` + testInventoryID + "\n"
   420  
   421  const testInventoryID = "SSSSSSSSSS-RRRRR"
   422  
   423  var kptFile = `
   424  apiVersion: kpt.dev/v1
   425  kind: Kptfile
   426  metadata:
   427    name: test1
   428  upstreamLock:
   429    type: git
   430    git:
   431      repo: git@github.com:seans3/blueprint-helloworld
   432      directory: /
   433      ref: master
   434  `
   435  
   436  var inventoryNamespace = "test-namespace"
   437  
   438  var resourceGroupInventory = `
   439  apiVersion: kpt.dev/v1alpha1
   440  kind: ResourceGroup
   441  metadata:
   442    name: foo
   443    namespace: test-namespace
   444    labels:
   445      cli-utils.sigs.k8s.io/inventory-id: SSSSSSSSSS-RRRRR
   446  `