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 }