internal/cmd/grow_container.go (121 lines of code) (raw):

package cmd import ( "context" "errors" "fmt" "strings" "time" "github.com/dustin/go-humanize" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/aws/ec2-macos-utils/internal/contextual" "github.com/aws/ec2-macos-utils/internal/diskutil" "github.com/aws/ec2-macos-utils/internal/diskutil/identifier" "github.com/aws/ec2-macos-utils/internal/diskutil/types" ) // growDefaultTimeout is the default maximum run duration of 5 minutes. This time limit should be sufficiently long // to allow macOS's diskutil command to execute for a variety of disk sizes. Anything beyond this limit will be treated // as unresponsive and the process will be terminated. This default time limit can be overridden with a flag. const growDefaultTimeout = 5 * time.Minute // growContainer is a struct for holding all information passed into the grow container command. type growContainer struct { dryrun bool id string timeout time.Duration } // growContainerCommand creates a new command which grows APFS containers to their maximum size. func growContainerCommand() *cobra.Command { cmd := &cobra.Command{ Use: "grow", Short: "resize container to max size", Long: strings.TrimSpace(` grow resizes the container to its maximum size using 'diskutil'. The container to operate on can be specified with its identifier (e.g. disk1 or /dev/disk1). The string 'root' may be provided to resize the OS's root volume. `), } // Set up the flags to be passed into the command growArgs := growContainer{} cmd.PersistentFlags().StringVar(&growArgs.id, "id", "", `container identifier to be resized or "root"`) cmd.PersistentFlags().BoolVar(&growArgs.dryrun, "dry-run", false, "run command without mutating changes") cmd.PersistentFlags().DurationVar(&growArgs.timeout, "timeout", growDefaultTimeout, "Set the timeout for the command (e.g. 30s, 1m, 1.5h), 0s will disable the timeout") _ = cmd.MarkPersistentFlagRequired("id") // Set up the command's pre-run to check for root permissions. // This is necessary since diskutil repairDisk requires root permissions to run. cmd.PreRunE = assertRootPrivileges // Set up the command's run function cmd.RunE = func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() if growArgs.timeout > 0 { timeoutBoundCtx, cancel := context.WithTimeout(ctx, growArgs.timeout) defer cancel() ctx = timeoutBoundCtx } product := contextual.Product(ctx) if product == nil { return errors.New("product required in context") } logrus.WithField("product", product).Info("Configuring diskutil for product") d, err := diskutil.ForProduct(product) if err != nil { return err } if growArgs.dryrun { d = diskutil.Dryrun(d) } logrus.WithField("args", growArgs).Debug("Running grow command with args") if err := run(ctx, d, growArgs); err != nil { if ctx.Err() == context.DeadlineExceeded { return errors.New("timeout exceeded") } return err } return nil } return cmd } // run attempts to grow the disk for the specified device identifier to its maximum size using diskutil.GrowContainer. func run(ctx context.Context, utility diskutil.DiskUtil, args growContainer) error { di, err := getTargetDiskInfo(ctx, utility, args.id) if err != nil { return fmt.Errorf("cannot grow container: %w", err) } logrus.WithField("device_id", di.DeviceIdentifier).Info("Attempting to grow container...") if err := diskutil.GrowContainer(ctx, utility, di); err != nil { // Don't treat FreeSpaceErrors as fatal, instead exit quietly since there's nothing else to do. if errors.As(err, &diskutil.FreeSpaceError{}) { logrus.WithField("id", args.id).Info("Nothing to do without free space, stopping command") return nil } return err } logrus.WithField("device_id", di.ParentWholeDisk).Info("Fetching updated information for device...") updatedDi, err := getTargetDiskInfo(ctx, utility, di.ParentWholeDisk) if err != nil { logrus.WithError(err).Error("Error while fetching updated disk information") return err } logrus.WithFields(logrus.Fields{ "device_id": di.DeviceIdentifier, "total_size": humanize.Bytes(updatedDi.TotalSize), }).Info("Successfully grew device to maximum size") return nil } // getTargetDiskInfo retrieves the disk info for the specified target identifier. If the identifier is "root", simply // return the disk information for "/". Otherwise, check if the identifier exists in the system partitions before // returning the disk information. func getTargetDiskInfo(ctx context.Context, du diskutil.DiskUtil, target string) (*types.DiskInfo, error) { if strings.EqualFold("root", target) { return du.Info(ctx, "/") } partitions, err := du.List(ctx, nil) if err != nil { return nil, fmt.Errorf("cannot list partitions: %w", err) } if err := validateDeviceID(target, partitions); err != nil { return nil, fmt.Errorf("invalid target: %w", err) } return du.Info(ctx, target) } // validateDeviceID verifies if the provided ID is a valid device identifier or device node. func validateDeviceID(id string, partitions *types.SystemPartitions) error { // Check if ID is provided if strings.TrimSpace(id) == "" { return errors.New("empty device id") } // Get the device identifier deviceID := identifier.ParseDiskID(id) if deviceID == "" { return errors.New("id does not match the expected device identifier format") } // Check the device directory for the given identifier for _, name := range partitions.AllDisks { if strings.EqualFold(name, deviceID) { return nil } } return errors.New("invalid device identifier") }