github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/commands/live/migrate/migratecmd.go (about)

     1  // Copyright 2020 The kpt Authors
     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  	"bytes"
    19  	"context"
    20  	goerrors "errors"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	initialization "github.com/GoogleContainerTools/kpt/commands/live/init"
    27  	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
    28  	"github.com/GoogleContainerTools/kpt/internal/errors"
    29  	"github.com/GoogleContainerTools/kpt/internal/pkg"
    30  	"github.com/GoogleContainerTools/kpt/internal/types"
    31  	"github.com/GoogleContainerTools/kpt/internal/util/argutil"
    32  	"github.com/GoogleContainerTools/kpt/internal/util/pathutil"
    33  	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
    34  	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
    35  	"github.com/GoogleContainerTools/kpt/pkg/live"
    36  	"github.com/spf13/cobra"
    37  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    38  	"k8s.io/cli-runtime/pkg/genericclioptions"
    39  	"k8s.io/klog/v2"
    40  	"k8s.io/kubectl/pkg/cmd/util"
    41  	"sigs.k8s.io/cli-utils/pkg/common"
    42  	"sigs.k8s.io/cli-utils/pkg/config"
    43  	"sigs.k8s.io/cli-utils/pkg/inventory"
    44  	"sigs.k8s.io/cli-utils/pkg/manifestreader"
    45  	"sigs.k8s.io/cli-utils/pkg/object"
    46  	"sigs.k8s.io/kustomize/kyaml/filesys"
    47  )
    48  
    49  // MigrateRunner encapsulates fields for the kpt migrate command.
    50  type Runner struct {
    51  	ctx       context.Context
    52  	Command   *cobra.Command
    53  	ioStreams genericclioptions.IOStreams
    54  	factory   util.Factory
    55  
    56  	dir             string
    57  	dryRun          bool
    58  	name            string
    59  	rgFile          string
    60  	force           bool
    61  	rgInvClientFunc func(util.Factory) (inventory.Client, error)
    62  	cmInvClientFunc func(util.Factory) (inventory.Client, error)
    63  	cmLoader        manifestreader.ManifestLoader
    64  	cmNotMigrated   bool // flag to determine if migration from ConfigMap has occurred
    65  }
    66  
    67  // NewRunner returns a pointer to an initial MigrateRunner structure.
    68  func NewRunner(
    69  	ctx context.Context,
    70  	factory util.Factory,
    71  	cmLoader manifestreader.ManifestLoader,
    72  	ioStreams genericclioptions.IOStreams,
    73  ) *Runner {
    74  	r := &Runner{
    75  		ctx:             ctx,
    76  		factory:         factory,
    77  		ioStreams:       ioStreams,
    78  		dryRun:          false,
    79  		cmLoader:        cmLoader,
    80  		rgInvClientFunc: rgInvClient,
    81  		cmInvClientFunc: cmInvClient,
    82  		dir:             "",
    83  	}
    84  	cmd := &cobra.Command{
    85  		Use:     "migrate [DIR | -]",
    86  		Short:   livedocs.MigrateShort,
    87  		Long:    livedocs.MigrateShort + "\n" + livedocs.MigrateLong,
    88  		Example: livedocs.MigrateExamples,
    89  		RunE: func(cmd *cobra.Command, args []string) error {
    90  			if len(args) == 0 {
    91  				// default to current working directory
    92  				args = append(args, ".")
    93  			}
    94  			fmt.Fprint(ioStreams.Out, "inventory migration...\n")
    95  			if err := r.Run(ioStreams.In, args); err != nil {
    96  				fmt.Fprint(ioStreams.Out, "failed\n")
    97  				fmt.Fprint(ioStreams.Out, "inventory migration...failed\n")
    98  				return err
    99  			}
   100  			fmt.Fprint(ioStreams.Out, "inventory migration...success\n")
   101  			return nil
   102  		},
   103  	}
   104  	cmd.Flags().StringVar(&r.name, "name", "", "Inventory object name")
   105  	cmd.Flags().BoolVar(&r.force, "force", false, "Set inventory values even if already set in Kptfile")
   106  	cmd.Flags().BoolVar(&r.dryRun, "dry-run", false, "Do not actually migrate, but show steps")
   107  	cmd.Flags().StringVar(&r.rgFile, "rg-file", rgfilev1alpha1.RGFileName, "The file path to the ResourceGroup object.")
   108  
   109  	r.Command = cmd
   110  	return r
   111  }
   112  
   113  // NewCommand returns the cobra command for the migrate command.
   114  func NewCommand(ctx context.Context, f util.Factory, cmLoader manifestreader.ManifestLoader,
   115  	ioStreams genericclioptions.IOStreams) *cobra.Command {
   116  	return NewRunner(ctx, f, cmLoader, ioStreams).Command
   117  }
   118  
   119  // Run executes the migration from the ConfigMap based inventory to the ResourceGroup
   120  // based inventory.
   121  func (mr *Runner) Run(reader io.Reader, args []string) error {
   122  	// Validate the number of arguments.
   123  	if len(args) > 1 {
   124  		return fmt.Errorf("too many arguments; migrate requires one directory argument (or stdin)")
   125  	}
   126  	// Validate argument is a directory.
   127  	if len(args) == 1 {
   128  		var err error
   129  		mr.dir, err = config.NormalizeDir(args[0])
   130  		if err != nil {
   131  			return err
   132  		}
   133  	}
   134  	// Store the stdin bytes if necessary so they can be used twice.
   135  	var stdinBytes []byte
   136  	var err error
   137  	if len(args) == 0 {
   138  		stdinBytes, err = io.ReadAll(reader)
   139  		if err != nil {
   140  			return err
   141  		}
   142  		if len(stdinBytes) == 0 {
   143  			return fmt.Errorf("no arguments means stdin has data; missing bytes on stdin")
   144  		}
   145  	}
   146  
   147  	// Apply the ResourceGroup CRD to the cluster, ignoring if it already exists.
   148  	if err := mr.applyCRD(); err != nil {
   149  		return err
   150  	}
   151  
   152  	// Check if we need to migrate from ConfigMap to ResourceGroup.
   153  	if err := mr.migrateCMToRG(stdinBytes, args); err != nil {
   154  		return err
   155  	}
   156  
   157  	// Migrate from Kptfile instead.
   158  	if mr.cmNotMigrated {
   159  		return mr.migrateKptfileToRG(args)
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  // applyCRD applies the ResourceGroup custom resource definition, returning an
   166  // error if one occurred. Ignores "AlreadyExists" error. Uses the definition
   167  // stored in the "rgCrd" variable.
   168  func (mr *Runner) applyCRD() error {
   169  	fmt.Fprint(mr.ioStreams.Out, "  ensuring ResourceGroup CRD exists in cluster...")
   170  	// Simply return early if this is a dry run
   171  	if mr.dryRun {
   172  		fmt.Fprintln(mr.ioStreams.Out, "success")
   173  		return nil
   174  	}
   175  	// Install the ResourceGroup CRD to the cluster.
   176  
   177  	err := (&live.ResourceGroupInstaller{
   178  		Factory: mr.factory,
   179  	}).InstallRG(mr.ctx)
   180  	if err == nil {
   181  		fmt.Fprintln(mr.ioStreams.Out, "success")
   182  	} else {
   183  		fmt.Fprintln(mr.ioStreams.Out, "failed")
   184  	}
   185  	return err
   186  }
   187  
   188  // retrieveConfigMapInv retrieves the ConfigMap inventory object or
   189  // an error if one occurred.
   190  func (mr *Runner) retrieveConfigMapInv(reader io.Reader, args []string) (inventory.Info, error) {
   191  	fmt.Fprint(mr.ioStreams.Out, "  retrieve the current ConfigMap inventory...")
   192  	cmReader, err := mr.cmLoader.ManifestReader(reader, args[0])
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	objs, err := cmReader.Read()
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	cmInvObj, _, err := inventory.SplitUnstructureds(objs)
   201  	if err != nil {
   202  		fmt.Fprintln(mr.ioStreams.Out, "no ConfigMap inventory...completed")
   203  		return nil, err
   204  	}
   205  
   206  	// cli-utils treats any resource that contains the inventory-id label as an inventory object. We should
   207  	// ignore any inventories that are stored as ResourceGroup resources since they do not need migration.
   208  	if cmInvObj.GetKind() == rgfilev1alpha1.ResourceGroupGVK().Kind {
   209  		// No ConfigMap inventory means the migration has already run before.
   210  		fmt.Fprintln(mr.ioStreams.Out, "no ConfigMap inventory...completed")
   211  		return nil, &inventory.NoInventoryObjError{}
   212  	}
   213  
   214  	cmInv := inventory.WrapInventoryInfoObj(cmInvObj)
   215  	fmt.Fprintf(mr.ioStreams.Out, "success (inventory-id: %s)\n", cmInv.ID())
   216  	return cmInv, nil
   217  }
   218  
   219  // retrieveInvObjs returns the object references from the passed
   220  // inventory object by querying the inventory object in the cluster,
   221  // or an error if one occurred.
   222  func (mr *Runner) retrieveInvObjs(cmInvClient inventory.Client,
   223  	invObj inventory.Info) ([]object.ObjMetadata, error) {
   224  	fmt.Fprint(mr.ioStreams.Out, "  retrieve ConfigMap inventory objs...")
   225  	cmObjs, err := cmInvClient.GetClusterObjs(invObj)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	fmt.Fprintf(mr.ioStreams.Out, "success (%d inventory objects)\n", len(cmObjs))
   230  	return cmObjs, nil
   231  }
   232  
   233  // migrateObjs stores the passed objects in the ResourceGroup inventory
   234  // object and applies the inventory object to the cluster. Returns
   235  // an error if one occurred.
   236  func (mr *Runner) migrateObjs(rgInvClient inventory.Client,
   237  	cmObjs []object.ObjMetadata, reader io.Reader, args []string) error {
   238  	if err := validateParams(reader, args); err != nil {
   239  		return err
   240  	}
   241  	fmt.Fprint(mr.ioStreams.Out, "  migrate inventory to ResourceGroup...")
   242  	if len(cmObjs) == 0 {
   243  		fmt.Fprint(mr.ioStreams.Out, "no inventory objects found\n")
   244  		return nil
   245  	}
   246  	if mr.dryRun {
   247  		fmt.Fprintln(mr.ioStreams.Out, "success")
   248  		return nil
   249  	}
   250  
   251  	path := args[0]
   252  	var err error
   253  	if args[0] != "-" {
   254  		path, err = argutil.ResolveSymlink(mr.ctx, path)
   255  		if err != nil {
   256  			return err
   257  		}
   258  	}
   259  
   260  	_, inv, err := live.Load(mr.factory, path, reader)
   261  	if err != nil {
   262  		return err
   263  	}
   264  
   265  	invInfo, err := live.ToInventoryInfo(inv)
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	_, err = rgInvClient.Merge(invInfo, cmObjs, mr.dryRunStrategy())
   271  	if err != nil {
   272  		return err
   273  	}
   274  	fmt.Fprint(mr.ioStreams.Out, "success\n")
   275  	return nil
   276  }
   277  
   278  // deleteConfigMapInv removes the passed inventory object from the
   279  // cluster. Returns an error if one occurred.
   280  func (mr *Runner) deleteConfigMapInv(cmInvClient inventory.Client,
   281  	invObj inventory.Info) error {
   282  	fmt.Fprint(mr.ioStreams.Out, "  deleting old ConfigMap inventory object...")
   283  	if err := cmInvClient.DeleteInventoryObj(invObj, mr.dryRunStrategy()); err != nil {
   284  		return err
   285  	}
   286  	fmt.Fprint(mr.ioStreams.Out, "success\n")
   287  	return nil
   288  }
   289  
   290  // deleteConfigMapFile deletes the ConfigMap template inventory file. This file
   291  // is usually named "inventory-template.yaml". This operation only happens if
   292  // the input was a directory argument (otherwise there is nothing to delete).
   293  // Returns an error if one occurs while deleting the file. Does NOT return an
   294  // error if the inventory template file is missing.
   295  func (mr *Runner) deleteConfigMapFile() error {
   296  	// Only delete the file if the input was a directory argument.
   297  	if len(mr.dir) > 0 {
   298  		cmFilename, _, err := common.ExpandDir(mr.dir)
   299  		if err != nil {
   300  			return err
   301  		}
   302  		if len(cmFilename) > 0 {
   303  			fmt.Fprintf(mr.ioStreams.Out, "deleting inventory template file: %s...", cmFilename)
   304  			if !mr.dryRun {
   305  				err = os.Remove(cmFilename)
   306  				if err != nil {
   307  					fmt.Fprint(mr.ioStreams.Out, "failed\n")
   308  					return err
   309  				}
   310  			}
   311  			fmt.Fprint(mr.ioStreams.Out, "success\n")
   312  		}
   313  	}
   314  	return nil
   315  }
   316  
   317  // dryRunStrategy returns the strategy to use based on user config
   318  func (mr *Runner) dryRunStrategy() common.DryRunStrategy {
   319  	if mr.dryRun {
   320  		return common.DryRunClient
   321  	}
   322  	return common.DryRunNone
   323  }
   324  
   325  // findResourceGroupInv returns the ResourceGroup inventory object from the
   326  // passed slice of objects, or nil and and error if there is a problem.
   327  func findResourceGroupInv(objs []*unstructured.Unstructured) (*unstructured.Unstructured, error) {
   328  	for _, obj := range objs {
   329  		isInv, err := live.IsResourceGroupInventory(obj)
   330  		if err != nil {
   331  			return nil, err
   332  		}
   333  		if isInv {
   334  			return obj, nil
   335  		}
   336  	}
   337  	return nil, fmt.Errorf("resource group inventory object not found")
   338  }
   339  
   340  // validateParams validates input parameters and returns error if any
   341  func validateParams(reader io.Reader, args []string) error {
   342  	if reader == nil && len(args) == 0 {
   343  		return fmt.Errorf("unable to build ManifestReader without both reader or args")
   344  	}
   345  	if len(args) > 1 {
   346  		return fmt.Errorf("expected one directory argument allowed; got (%s)", args)
   347  	}
   348  	return nil
   349  }
   350  
   351  func rgInvClient(factory util.Factory) (inventory.Client, error) {
   352  	return inventory.NewClient(factory, live.WrapInventoryObj, live.InvToUnstructuredFunc, inventory.StatusPolicyAll, live.ResourceGroupGVK)
   353  }
   354  
   355  func cmInvClient(factory util.Factory) (inventory.Client, error) {
   356  	return inventory.NewClient(factory, inventory.WrapInventoryObj, inventory.InvInfoToConfigMap, inventory.StatusPolicyAll, live.ResourceGroupGVK)
   357  }
   358  
   359  // migrateKptfileToRG extracts inventory information from a package's Kptfile
   360  // into an external resourcegroup file.
   361  func (mr *Runner) migrateKptfileToRG(args []string) error {
   362  	const op errors.Op = "migratecmd.migrateKptfileToRG"
   363  	klog.V(4).Infoln("attempting to migrate from Kptfile inventory")
   364  	fmt.Fprint(mr.ioStreams.Out, "  reading existing Kptfile...")
   365  	if !mr.dryRun {
   366  		dir, _, err := pathutil.ResolveAbsAndRelPaths(args[0])
   367  		if err != nil {
   368  			return err
   369  		}
   370  		p, err := pkg.New(filesys.FileSystemOrOnDisk{}, dir)
   371  		if err != nil {
   372  			return err
   373  		}
   374  		kf, err := p.Kptfile()
   375  		if err != nil {
   376  			return err
   377  		}
   378  
   379  		if _, err := kptfileutil.ValidateInventory(kf.Inventory); err != nil {
   380  			// Kptfile does not contain inventory: migration is not needed.
   381  			return nil
   382  		}
   383  
   384  		// Make sure resourcegroup file does not exist.
   385  		_, rgFileErr := os.Stat(filepath.Join(dir, mr.rgFile))
   386  		switch {
   387  		case rgFileErr == nil:
   388  			return errors.E(op, errors.IO, types.UniquePath(dir), "the resourcegroup file already exists and inventory information cannot be migrated")
   389  		case err != nil && !goerrors.Is(err, os.ErrNotExist):
   390  			return errors.E(op, errors.IO, types.UniquePath(dir), err)
   391  		}
   392  
   393  		err = (&initialization.ConfigureInventoryInfo{
   394  			Pkg:         p,
   395  			Factory:     mr.factory,
   396  			Quiet:       true,
   397  			Name:        kf.Inventory.Name,
   398  			InventoryID: kf.Inventory.InventoryID,
   399  			RGFileName:  mr.rgFile,
   400  			Force:       true,
   401  		}).Run(mr.ctx)
   402  
   403  		if err != nil {
   404  			return err
   405  		}
   406  	}
   407  	fmt.Fprint(mr.ioStreams.Out, "success\n")
   408  	return nil
   409  }
   410  
   411  // migrateCMToRG migrates from ConfigMap to resourcegroup object.
   412  func (mr *Runner) migrateCMToRG(stdinBytes []byte, args []string) error {
   413  	// Create the inventory clients for reading inventories based on RG and
   414  	// ConfigMap.
   415  	rgInvClient, err := mr.rgInvClientFunc(mr.factory)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	cmInvClient, err := mr.cmInvClientFunc(mr.factory)
   420  	if err != nil {
   421  		return err
   422  	}
   423  	// Retrieve the current ConfigMap inventory objects.
   424  	cmInvObj, err := mr.retrieveConfigMapInv(bytes.NewReader(stdinBytes), args)
   425  	if err != nil {
   426  		if _, ok := err.(*inventory.NoInventoryObjError); ok {
   427  			// No ConfigMap inventory means the migration has already run before.
   428  			klog.V(4).Infoln("swallowing no ConfigMap inventory error")
   429  			mr.cmNotMigrated = true
   430  			return nil
   431  		}
   432  		klog.V(4).Infof("error retrieving ConfigMap inventory object: %s", err)
   433  		return err
   434  	}
   435  	cmInventoryID := cmInvObj.ID()
   436  	klog.V(4).Infof("previous inventoryID: %s", cmInventoryID)
   437  	// Create ResourceGroup object file locallly (e.g. namespace, name, id).
   438  	if err := mr.createRGfile(mr.ctx, args, cmInventoryID); err != nil {
   439  		return err
   440  	}
   441  	cmObjs, err := mr.retrieveInvObjs(cmInvClient, cmInvObj)
   442  	if err != nil {
   443  		return err
   444  	}
   445  	if len(cmObjs) > 0 {
   446  		// Migrate the ConfigMap inventory objects to a ResourceGroup custom resource.
   447  		if err = mr.migrateObjs(rgInvClient, cmObjs, bytes.NewReader(stdinBytes), args); err != nil {
   448  			return err
   449  		}
   450  		// Delete the old ConfigMap inventory object.
   451  		if err = mr.deleteConfigMapInv(cmInvClient, cmInvObj); err != nil {
   452  			return err
   453  		}
   454  	}
   455  	return mr.deleteConfigMapFile()
   456  }
   457  
   458  // createRGfile writes the inventory information into the resourcegroup object.
   459  func (mr *Runner) createRGfile(ctx context.Context, args []string, prevID string) error {
   460  	fmt.Fprint(mr.ioStreams.Out, "  creating ResourceGroup object file...")
   461  	if !mr.dryRun {
   462  		dir, _, err := pathutil.ResolveAbsAndRelPaths(args[0])
   463  		if err != nil {
   464  			return err
   465  		}
   466  		p, err := pkg.New(filesys.FileSystemOrOnDisk{}, dir)
   467  		if err != nil {
   468  			return err
   469  		}
   470  		err = (&initialization.ConfigureInventoryInfo{
   471  			Pkg:         p,
   472  			Factory:     mr.factory,
   473  			Quiet:       true,
   474  			InventoryID: prevID,
   475  			RGFileName:  mr.rgFile,
   476  			Force:       mr.force,
   477  		}).Run(ctx)
   478  
   479  		if err != nil {
   480  			var invExistsError *initialization.InvExistsError
   481  			if errors.As(err, &invExistsError) {
   482  				fmt.Fprint(mr.ioStreams.Out, "values already exist...")
   483  			} else {
   484  				return err
   485  			}
   486  		}
   487  	}
   488  	fmt.Fprint(mr.ioStreams.Out, "success\n")
   489  	return nil
   490  }