Skip to content

Commit 58b5e35

Browse files
authored
feat: enable migrating from Kptfile & CM to resourcegroup inventories (#2705)
This commit enables `kpt live migrate` to migrate inventory information from either a ConfigMap or Kptfile to a separate ResourceGroup file. This functionality is currently behind a feature gate and is not exposed to the user via any CLI flags. Enabling of this feature to users will be done later.
1 parent 0fbcbc7 commit 58b5e35

File tree

2 files changed

+322
-1
lines changed

2 files changed

+322
-1
lines changed

internal/cmdmigrate/migratecmd.go

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,20 @@ package cmdmigrate
66
import (
77
"bytes"
88
"context"
9-
"errors"
9+
goerrors "errors"
1010
"fmt"
1111
"io"
1212
"io/ioutil"
1313
"os"
14+
"path/filepath"
1415

1516
"github.com/GoogleContainerTools/kpt/internal/cmdliveinit"
1617
"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
18+
"github.com/GoogleContainerTools/kpt/internal/errors"
1719
"github.com/GoogleContainerTools/kpt/internal/pkg"
20+
"github.com/GoogleContainerTools/kpt/internal/types"
1821
"github.com/GoogleContainerTools/kpt/internal/util/argutil"
22+
"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
1923
"github.com/GoogleContainerTools/kpt/pkg/live"
2024
"github.com/spf13/cobra"
2125
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -39,10 +43,12 @@ type MigrateRunner struct {
3943
dir string
4044
dryRun bool
4145
name string
46+
rgFile string
4247
force bool
4348
rgInvClientFunc func(util.Factory) (inventory.InventoryClient, error)
4449
cmInvClientFunc func(util.Factory) (inventory.InventoryClient, error)
4550
cmLoader manifestreader.ManifestLoader
51+
cmNotMigrated bool // flag to determine if migration from ConfigMap has occurred
4652
}
4753

4854
// NewRunner returns a pointer to an initial MigrateRunner structure.
@@ -96,6 +102,15 @@ func NewCommand(ctx context.Context, f util.Factory, cmLoader manifestreader.Man
96102
// Run executes the migration from the ConfigMap based inventory to the ResourceGroup
97103
// based inventory.
98104
func (mr *MigrateRunner) Run(reader io.Reader, args []string) error {
105+
// Use ResourceGroup file for inventory logic if the resourcegroup file
106+
// is set directly. For this feature gate, the resourcegroup must be directly set
107+
// through our tests since we are not exposing this through the command surface as a
108+
// flag, currently. When we promote this, the resourcegroup filename can be empty and
109+
// the default filename value will be inferred/used.
110+
if mr.rgFile != "" {
111+
return mr.runLiveMigrateWithRGFile(reader, args)
112+
}
113+
99114
// Validate the number of arguments.
100115
if len(args) > 1 {
101116
return fmt.Errorf("too many arguments; migrate requires one directory argument (or stdin)")
@@ -384,3 +399,176 @@ func rgInvClient(factory util.Factory) (inventory.InventoryClient, error) {
384399
func cmInvClient(factory util.Factory) (inventory.InventoryClient, error) {
385400
return inventory.NewInventoryClient(factory, inventory.WrapInventoryObj, inventory.InvInfoToConfigMap)
386401
}
402+
403+
// func runLiveMigrateWithRGFile is a modified version of MigrateRunner.Run that stores the
404+
// package inventory information in a separate resourcegroup file. The logic for this is branched into
405+
// a separate function to enable feature gating.
406+
func (mr *MigrateRunner) runLiveMigrateWithRGFile(reader io.Reader, args []string) error {
407+
// Validate the number of arguments.
408+
if len(args) > 1 {
409+
return fmt.Errorf("too many arguments; migrate requires one directory argument (or stdin)")
410+
}
411+
// Validate argument is a directory.
412+
if len(args) == 1 {
413+
var err error
414+
mr.dir, err = config.NormalizeDir(args[0])
415+
if err != nil {
416+
return err
417+
}
418+
}
419+
// Store the stdin bytes if necessary so they can be used twice.
420+
var stdinBytes []byte
421+
var err error
422+
if len(args) == 0 {
423+
stdinBytes, err = ioutil.ReadAll(reader)
424+
if err != nil {
425+
return err
426+
}
427+
if len(stdinBytes) == 0 {
428+
return fmt.Errorf("no arguments means stdin has data; missing bytes on stdin")
429+
}
430+
}
431+
432+
// Apply the ResourceGroup CRD to the cluster, ignoring if it already exists.
433+
if err := mr.applyCRD(); err != nil {
434+
return err
435+
}
436+
437+
// Check if we need to migrate from ConfigMap to ResourceGroup.
438+
if err := mr.migrateCMToRG(stdinBytes, args); err != nil {
439+
return err
440+
}
441+
442+
// Migrate from Kptfile instead.
443+
if mr.cmNotMigrated {
444+
return mr.migrateKptfileToRG(args)
445+
}
446+
447+
return nil
448+
}
449+
450+
// migrateKptfileToRG extracts inventory information from a package's Kptfile
451+
// into an external resourcegroup file.
452+
func (mr *MigrateRunner) migrateKptfileToRG(args []string) error {
453+
const op errors.Op = "migratecmd.migrateKptfileToRG"
454+
klog.V(4).Infoln("attempting to migrate from Kptfile inventory")
455+
fmt.Fprint(mr.ioStreams.Out, " reading existing Kptfile...")
456+
if !mr.dryRun {
457+
dir := args[0]
458+
p, err := pkg.New(dir)
459+
if err != nil {
460+
return err
461+
}
462+
kf, err := p.Kptfile()
463+
if err != nil {
464+
return err
465+
}
466+
467+
if _, err := kptfileutil.ValidateInventory(kf.Inventory); err != nil {
468+
// Invalid Kptfile.
469+
return err
470+
}
471+
472+
// Make sure resourcegroup file does not exist.
473+
_, rgFileErr := os.Stat(filepath.Join(dir, mr.rgFile))
474+
switch {
475+
case rgFileErr == nil:
476+
return errors.E(op, errors.IO, types.UniquePath(dir), "the resourcegroup file already exists and inventory information cannot be migrated")
477+
case err != nil && !goerrors.Is(err, os.ErrNotExist):
478+
return errors.E(op, errors.IO, types.UniquePath(dir), err)
479+
}
480+
481+
err = (&cmdliveinit.ConfigureInventoryInfo{
482+
Pkg: p,
483+
Factory: mr.factory,
484+
Quiet: true,
485+
Name: kf.Inventory.Name,
486+
InventoryID: kf.Inventory.InventoryID,
487+
RGFileName: mr.rgFile,
488+
Force: true,
489+
}).Run(mr.ctx)
490+
491+
if err != nil {
492+
return err
493+
}
494+
}
495+
fmt.Fprint(mr.ioStreams.Out, "success\n")
496+
return nil
497+
}
498+
499+
// migrateCMToRG migrates from ConfigMap to resourcegroup object.
500+
func (mr *MigrateRunner) migrateCMToRG(stdinBytes []byte, args []string) error {
501+
// Create the inventory clients for reading inventories based on RG and
502+
// ConfigMap.
503+
rgInvClient, err := mr.rgInvClientFunc(mr.factory)
504+
if err != nil {
505+
return err
506+
}
507+
cmInvClient, err := mr.cmInvClientFunc(mr.factory)
508+
if err != nil {
509+
return err
510+
}
511+
// Retrieve the current ConfigMap inventory objects.
512+
cmInvObj, err := mr.retrieveConfigMapInv(bytes.NewReader(stdinBytes), args)
513+
if err != nil {
514+
if _, ok := err.(inventory.NoInventoryObjError); ok {
515+
// No ConfigMap inventory means the migration has already run before.
516+
klog.V(4).Infoln("swallowing no ConfigMap inventory error")
517+
mr.cmNotMigrated = true
518+
return nil
519+
}
520+
klog.V(4).Infof("error retrieving ConfigMap inventory object: %s", err)
521+
return err
522+
}
523+
cmInventoryID := cmInvObj.ID()
524+
klog.V(4).Infof("previous inventoryID: %s", cmInventoryID)
525+
// Create ResourceGroup object file locallly (e.g. namespace, name, id).
526+
if err := mr.createRGfile(mr.ctx, args, cmInventoryID); err != nil {
527+
return err
528+
}
529+
cmObjs, err := mr.retrieveInvObjs(cmInvClient, cmInvObj)
530+
if err != nil {
531+
return err
532+
}
533+
if len(cmObjs) > 0 {
534+
// Migrate the ConfigMap inventory objects to a ResourceGroup custom resource.
535+
if err = mr.migrateObjs(rgInvClient, cmObjs, bytes.NewReader(stdinBytes), args); err != nil {
536+
return err
537+
}
538+
// Delete the old ConfigMap inventory object.
539+
if err = mr.deleteConfigMapInv(cmInvClient, cmInvObj); err != nil {
540+
return err
541+
}
542+
}
543+
return mr.deleteConfigMapFile()
544+
}
545+
546+
// createRGfile writes the inventory information into the resourcegroup object.
547+
func (mr *MigrateRunner) createRGfile(ctx context.Context, args []string, prevID string) error {
548+
fmt.Fprint(mr.ioStreams.Out, " creating ResourceGroup object file...")
549+
if !mr.dryRun {
550+
p, err := pkg.New(args[0])
551+
if err != nil {
552+
return err
553+
}
554+
err = (&cmdliveinit.ConfigureInventoryInfo{
555+
Pkg: p,
556+
Factory: mr.factory,
557+
Quiet: true,
558+
InventoryID: prevID,
559+
RGFileName: mr.rgFile,
560+
Force: mr.force,
561+
}).Run(ctx)
562+
563+
if err != nil {
564+
var invExistsError *cmdliveinit.InvExistsError
565+
if errors.As(err, &invExistsError) {
566+
fmt.Fprint(mr.ioStreams.Out, "values already exist...")
567+
} else {
568+
return err
569+
}
570+
}
571+
}
572+
fmt.Fprint(mr.ioStreams.Out, "success\n")
573+
return nil
574+
}

internal/cmdmigrate/migratecmd_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/GoogleContainerTools/kpt/internal/pkg"
1313
"github.com/GoogleContainerTools/kpt/internal/printer/fake"
14+
rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
1415
"github.com/stretchr/testify/assert"
1516
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1617
"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -177,6 +178,118 @@ func TestKptMigrate_updateKptfile(t *testing.T) {
177178
}
178179
}
179180

181+
func TestKptMigrate_migrateKptfileToRG(t *testing.T) {
182+
testCases := map[string]struct {
183+
kptfile string
184+
rgFilename string
185+
resourcegroup string
186+
dryRun bool
187+
isError bool
188+
}{
189+
"Missing Kptfile is an error": {
190+
kptfile: "",
191+
rgFilename: "resourcegroup.yaml",
192+
dryRun: false,
193+
isError: true,
194+
},
195+
"Kptfile with existing inventory will create ResourceGroup": {
196+
kptfile: kptFileWithInventory,
197+
rgFilename: "resourcegroup.yaml",
198+
dryRun: false,
199+
isError: false,
200+
},
201+
"ResopurceGroup file already exists will error": {
202+
kptfile: kptFileWithInventory,
203+
rgFilename: "resourcegroup.yaml",
204+
resourcegroup: resourceGroupInventory,
205+
dryRun: false,
206+
isError: true,
207+
},
208+
"Dry-run will not fill in inventory fields": {
209+
kptfile: kptFile,
210+
rgFilename: "resourcegroup.yaml",
211+
dryRun: true,
212+
isError: false,
213+
},
214+
"Custom ResourceGroup file will be generated": {
215+
kptfile: kptFileWithInventory,
216+
rgFilename: "custom-rg.yaml",
217+
dryRun: false,
218+
isError: false,
219+
},
220+
}
221+
222+
for tn, tc := range testCases {
223+
t.Run(tn, func(t *testing.T) {
224+
// Set up fake test factory
225+
tf := cmdtesting.NewTestFactory().WithNamespace(inventoryNamespace)
226+
defer tf.Cleanup()
227+
ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
228+
229+
// Set up temp directory with Ktpfile
230+
dir, err := ioutil.TempDir("", "kpt-migrate-test")
231+
assert.NoError(t, err)
232+
p := filepath.Join(dir, "Kptfile")
233+
err = ioutil.WriteFile(p, []byte(tc.kptfile), 0600)
234+
assert.NoError(t, err)
235+
236+
if tc.resourcegroup != "" {
237+
p := filepath.Join(dir, tc.rgFilename)
238+
err = ioutil.WriteFile(p, []byte(tc.resourcegroup), 0600)
239+
assert.NoError(t, err)
240+
}
241+
242+
ctx := fake.CtxWithDefaultPrinter()
243+
// Create MigrateRunner and call "updateKptfile"
244+
cmLoader := manifestreader.NewManifestLoader(tf)
245+
migrateRunner := NewRunner(ctx, tf, cmLoader, ioStreams)
246+
migrateRunner.dryRun = tc.dryRun
247+
migrateRunner.rgFile = tc.rgFilename
248+
migrateRunner.cmInvClientFunc = func(factory util.Factory) (inventory.InventoryClient, error) {
249+
return inventory.NewFakeInventoryClient([]object.ObjMetadata{}), nil
250+
}
251+
err = migrateRunner.migrateKptfileToRG([]string{dir})
252+
// Check if there should be an error
253+
if tc.isError {
254+
if err == nil {
255+
t.Fatalf("expected error but received none")
256+
}
257+
return
258+
}
259+
assert.NoError(t, err)
260+
kf, err := pkg.ReadKptfile(dir)
261+
if !assert.NoError(t, err) {
262+
t.FailNow()
263+
}
264+
265+
rg, err := pkg.ReadRGFile(dir, migrateRunner.rgFile)
266+
if !tc.dryRun && !assert.NoError(t, err) {
267+
t.FailNow()
268+
}
269+
270+
// Ensure the Kptfile does not contain inventory information.
271+
if !assert.Nil(t, kf.Inventory) {
272+
t.Errorf("inventory information should not be set in Kptfile")
273+
}
274+
275+
if !tc.dryRun {
276+
if rg == nil {
277+
t.Fatalf("unable to read ResourceGroup file")
278+
}
279+
assert.Equal(t, inventoryNamespace, rg.ObjectMeta.Namespace)
280+
if len(rg.ObjectMeta.Name) == 0 {
281+
t.Errorf("inventory name not set in Kptfile")
282+
}
283+
if rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel] != testInventoryID {
284+
t.Errorf("inventory id not set correctly in ResourceGroup: %s", rg.ObjectMeta.Labels[rgfilev1alpha1.RGInventoryIDLabel])
285+
}
286+
} else if rg != nil {
287+
t.Errorf("inventory shouldn't be set during dryrun")
288+
}
289+
})
290+
}
291+
}
292+
180293
func TestKptMigrate_retrieveConfigMapInv(t *testing.T) {
181294
testCases := map[string]struct {
182295
configMap string
@@ -297,6 +410,16 @@ func TestKptMigrate_migrateObjs(t *testing.T) {
297410
},
298411
isError: false,
299412
},
413+
"Kptfile does not have inventory is valid": {
414+
invObj: kptFile,
415+
objs: []object.ObjMetadata{},
416+
isError: false,
417+
},
418+
"One migrate object is valid with inventory in Kptfile": {
419+
invObj: kptFileWithInventory,
420+
objs: []object.ObjMetadata{object.UnstructuredToObjMetadata(pod1)},
421+
isError: false,
422+
},
300423
}
301424

302425
for tn, tc := range testCases {
@@ -375,3 +498,13 @@ upstreamLock:
375498
`
376499

377500
var inventoryNamespace = "test-namespace"
501+
502+
var resourceGroupInventory = `
503+
apiVersion: kpt.dev/v1alpha1
504+
kind: ResourceGroup
505+
metadata:
506+
name: foo
507+
namespace: test-namespace
508+
labels:
509+
cli-utils.sigs.k8s.io/inventory-id: SSSSSSSSSS-RRRRR
510+
`

0 commit comments

Comments
 (0)