github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/plugin/pathutil.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package plugin
    21  
    22  import (
    23  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"syscall"
    28  
    29  	"github.com/pkg/errors"
    30  	"k8s.io/klog/v2"
    31  
    32  	"github.com/1aal/kubeblocks/pkg/cli/util"
    33  )
    34  
    35  type move struct {
    36  	from, to string
    37  }
    38  
    39  func findMoveTargets(fromDir, toDir string, fo FileOperation) ([]move, error) {
    40  	if fo.To != filepath.Clean(fo.To) {
    41  		return nil, errors.Errorf("the provided path is not clean, %q should be %q", fo.To, filepath.Clean(fo.To))
    42  	}
    43  	fromDir, err := filepath.Abs(fromDir)
    44  	if err != nil {
    45  		return nil, errors.Wrap(err, "could not get the relative path for the move src")
    46  	}
    47  
    48  	klog.V(4).Infof("Trying to move single file directly from=%q to=%q with file operation=%#v", fromDir, toDir, fo)
    49  	if m, ok, err := getDirectMove(fromDir, toDir, fo); err != nil {
    50  		return nil, errors.Wrap(err, "failed to detect single move operation")
    51  	} else if ok {
    52  		klog.V(3).Infof("Detected single move from file operation=%#v", fo)
    53  		return []move{m}, nil
    54  	}
    55  
    56  	klog.V(4).Infoln("Wasn't a single file, proceeding with Glob move")
    57  	newDir, err := filepath.Abs(filepath.Join(filepath.FromSlash(toDir), filepath.FromSlash(fo.To)))
    58  	if err != nil {
    59  		return nil, errors.Wrap(err, "could not get the relative path for the move dst")
    60  	}
    61  
    62  	gl, err := filepath.Glob(filepath.Join(filepath.FromSlash(fromDir), filepath.FromSlash(fo.From)))
    63  	if err != nil {
    64  		return nil, errors.Wrap(err, "could not get files using a glob string")
    65  	}
    66  	if len(gl) == 0 {
    67  		return nil, errors.Errorf("no files in the plugin archive matched the glob pattern=%s", fo.From)
    68  	}
    69  
    70  	moves := make([]move, 0, len(gl))
    71  	for _, v := range gl {
    72  		newPath := filepath.Join(newDir, filepath.Base(filepath.FromSlash(v)))
    73  		// Check secure path
    74  		m := move{from: v, to: newPath}
    75  		if !isMoveAllowed(fromDir, toDir, m) {
    76  			return nil, errors.Errorf("can't move, move target %v is not a subpath from=%q, to=%q", m, fromDir, toDir)
    77  		}
    78  		moves = append(moves, m)
    79  	}
    80  	return moves, nil
    81  }
    82  
    83  func getDirectMove(fromDir, toDir string, fo FileOperation) (move, bool, error) {
    84  	var m move
    85  	fromDir, err := filepath.Abs(fromDir)
    86  	if err != nil {
    87  		return m, false, errors.Wrap(err, "could not get the relative path for the move src")
    88  	}
    89  
    90  	toDir, err = filepath.Abs(toDir)
    91  	if err != nil {
    92  		return m, false, errors.Wrap(err, "could not get the relative path for the move dst")
    93  	}
    94  
    95  	// Check file (not a Glob)
    96  	fromFilePath := filepath.Clean(filepath.Join(fromDir, fo.From))
    97  	_, err = os.Stat(fromFilePath)
    98  	if err != nil {
    99  		return m, false, nil
   100  	}
   101  
   102  	// If target is empty use old file name.
   103  	if filepath.Clean(fo.To) == "." {
   104  		fo.To = filepath.Base(fromFilePath)
   105  	}
   106  
   107  	// Build new file name
   108  	toFilePath, err := filepath.Abs(filepath.Join(filepath.FromSlash(toDir), filepath.FromSlash(fo.To)))
   109  	if err != nil {
   110  		return m, false, errors.Wrap(err, "could not get the relative path for the move dst")
   111  	}
   112  
   113  	// Check sane path
   114  	m = move{from: fromFilePath, to: toFilePath}
   115  	if !isMoveAllowed(fromDir, toDir, m) {
   116  		return move{}, false, errors.Errorf("can't move, move target %v is out of bounds from=%q, to=%q", m, fromDir, toDir)
   117  	}
   118  
   119  	return m, true, nil
   120  }
   121  
   122  func isMoveAllowed(fromBase, toBase string, m move) bool {
   123  	_, okFrom := IsSubPath(fromBase, m.from)
   124  	_, okTo := IsSubPath(toBase, m.to)
   125  	return okFrom && okTo
   126  }
   127  
   128  func moveFiles(fromDir, toDir string, fo FileOperation) error {
   129  	klog.V(4).Infof("Finding move targets from %q to %q with file operation=%#v", fromDir, toDir, fo)
   130  	moves, err := findMoveTargets(fromDir, toDir, fo)
   131  	if err != nil {
   132  		return errors.Wrap(err, "could not find move targets")
   133  	}
   134  
   135  	for _, m := range moves {
   136  		klog.V(2).Infof("Move file from %q to %q", m.from, m.to)
   137  		if err := os.MkdirAll(filepath.Dir(m.to), 0o755); err != nil {
   138  			return errors.Wrapf(err, "failed to create move path %q", filepath.Dir(m.to))
   139  		}
   140  
   141  		if err = renameOrCopy(m.from, m.to); err != nil {
   142  			return errors.Wrapf(err, "could not rename/copy file from %q to %q", m.from, m.to)
   143  		}
   144  	}
   145  	klog.V(4).Infoln("Move operations completed")
   146  	return nil
   147  }
   148  
   149  func moveAllFiles(fromDir, toDir string, fos []FileOperation) error {
   150  	for _, fo := range fos {
   151  		if err := moveFiles(fromDir, toDir, fo); err != nil {
   152  			return errors.Wrap(err, "failed moving files")
   153  		}
   154  	}
   155  	return nil
   156  }
   157  
   158  // moveToInstallDir moves plugins from srcDir to dstDir (created in this method) with given FileOperation.
   159  func moveToInstallDir(srcDir, installDir string, fos []FileOperation) error {
   160  	installationDir := filepath.Dir(installDir)
   161  	klog.V(4).Infof("Creating directory %q", installationDir)
   162  	if err := os.MkdirAll(installationDir, 0o755); err != nil {
   163  		return errors.Wrapf(err, "error creating directory at %q", installationDir)
   164  	}
   165  
   166  	tmp, err := os.MkdirTemp("", "kbcli-temp-move")
   167  	klog.V(4).Infof("Creating temp plugin move operations dir %q", tmp)
   168  	if err != nil {
   169  		return errors.Wrap(err, "failed to find a temporary director")
   170  	}
   171  	defer os.RemoveAll(tmp)
   172  
   173  	if err = moveAllFiles(srcDir, tmp, fos); err != nil {
   174  		return errors.Wrap(err, "failed to move files")
   175  	}
   176  
   177  	klog.V(2).Infof("Move directory %q to %q", tmp, installDir)
   178  	if err = renameOrCopy(tmp, installDir); err != nil {
   179  		defer func() {
   180  			klog.V(3).Info("Cleaning up installation directory due to error during copying files")
   181  			os.Remove(installDir)
   182  		}()
   183  		return errors.Wrapf(err, "could not rename/copy directory %q to %q", tmp, installDir)
   184  	}
   185  	return nil
   186  }
   187  
   188  // renameOrCopy tries to rename a dir or file. If rename is not supported, a manual copy will be performed.
   189  // Existing files at "to" will be deleted.
   190  func renameOrCopy(from, to string) error {
   191  	// Try atomic rename (does not work cross partition).
   192  	fi, err := os.Stat(to)
   193  	if err != nil && !os.IsNotExist(err) {
   194  		return errors.Wrapf(err, "error checking move target dir %q", to)
   195  	}
   196  	if fi != nil && fi.IsDir() {
   197  		klog.V(4).Infof("There's already a directory at move target %q. deleting.", to)
   198  		if err := os.RemoveAll(to); err != nil {
   199  			return errors.Wrapf(err, "error cleaning up dir %q", to)
   200  		}
   201  		klog.V(4).Infof("Move target directory %q cleaned up", to)
   202  	}
   203  
   204  	err = os.Rename(from, to)
   205  	// Fallback for invalid cross-device link (errno:18).
   206  	if isCrossDeviceRenameErr(err) {
   207  		klog.V(2).Infof("Cross-device link error while copying, fallback to manual copy")
   208  		return errors.Wrap(copyTree(from, to), "failed to copy directory tree as a fallback")
   209  	}
   210  	return err
   211  }
   212  
   213  // copyTree copies files or directories, recursively.
   214  func copyTree(from, to string) (err error) {
   215  	return filepath.Walk(from, func(path string, info os.FileInfo, err error) error {
   216  		if err != nil {
   217  			return err
   218  		}
   219  		newPath, _ := ReplaceBase(path, from, to)
   220  		if info.IsDir() {
   221  			klog.V(4).Infof("Creating new dir %q", newPath)
   222  			err = os.MkdirAll(newPath, info.Mode())
   223  		} else {
   224  			klog.V(4).Infof("Copying file %q", newPath)
   225  			err = copyFile(path, newPath, info.Mode())
   226  		}
   227  		return err
   228  	})
   229  }
   230  
   231  func copyFile(source, dst string, mode os.FileMode) (err error) {
   232  	sf, err := os.Open(source)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	defer sf.Close()
   237  
   238  	df, err := os.Create(dst)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	defer df.Close()
   243  
   244  	_, err = io.Copy(df, sf)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	return os.Chmod(dst, mode)
   249  }
   250  
   251  // isCrossDeviceRenameErr determines if an os.Rename error is due to cross-fs/drive/volume copying.
   252  func isCrossDeviceRenameErr(err error) bool {
   253  	le, ok := err.(*os.LinkError)
   254  	if !ok {
   255  		return false
   256  	}
   257  	errno, ok := le.Err.(syscall.Errno)
   258  	if !ok {
   259  		return false
   260  	}
   261  	return (util.IsWindows() && errno == 17) || // syscall.ERROR_NOT_SAME_DEVICE
   262  		(!util.IsWindows() && errno == 18) // syscall.EXDEV
   263  }
   264  
   265  // IsSubPath checks if the extending path is an extension of the basePath, it will return the extending path
   266  // elements. Both paths have to be absolute or have the same root directory. The remaining path elements
   267  func IsSubPath(basePath, subPath string) (string, bool) {
   268  	extendingPath, err := filepath.Rel(basePath, subPath)
   269  	if err != nil {
   270  		return "", false
   271  	}
   272  	if strings.HasPrefix(extendingPath, "..") {
   273  		return "", false
   274  	}
   275  	return extendingPath, true
   276  }
   277  
   278  // ReplaceBase returns a replacement path with replacement as a base of the path instead of the old base. a/b/c, a, d -> d/b/c
   279  func ReplaceBase(path, old, replacement string) (string, error) {
   280  	extendingPath, ok := IsSubPath(old, path)
   281  	if !ok {
   282  		return "", errors.Errorf("can't replace %q in %q, it is not a subpath", old, path)
   283  	}
   284  	return filepath.Join(replacement, extendingPath), nil
   285  }
   286  
   287  // CanonicalPluginName resolves a plugin's index and name from input string.
   288  // If an index is not specified, the default index name is assumed.
   289  func CanonicalPluginName(in string) (string, string) {
   290  	if strings.Count(in, "/") == 0 {
   291  		return DefaultIndexName, in
   292  	}
   293  	p := strings.SplitN(in, "/", 2)
   294  	return p[0], p[1]
   295  }
   296  
   297  func createOrUpdateLink(binDir, binary, plugin string) error {
   298  	dst := filepath.Join(binDir, pluginNameToBin(plugin, util.IsWindows()))
   299  
   300  	if err := removeLink(dst); err != nil {
   301  		return errors.Wrap(err, "failed to remove old symlink")
   302  	}
   303  	if _, err := os.Stat(binary); os.IsNotExist(err) {
   304  		return errors.Wrapf(err, "can't create symbolic link, source binary (%q) cannot be found in extracted archive", binary)
   305  	}
   306  
   307  	// Create new
   308  	klog.V(2).Infof("Creating symlink to %q at %q", binary, dst)
   309  	if err := os.Symlink(binary, dst); err != nil {
   310  		return errors.Wrapf(err, "failed to create a symlink from %q to %q", binary, dst)
   311  	}
   312  	klog.V(2).Infof("Created symlink at %q", dst)
   313  
   314  	return nil
   315  }
   316  
   317  // removeLink removes a symlink reference if exists.
   318  func removeLink(path string) error {
   319  	fi, err := os.Lstat(path)
   320  	if os.IsNotExist(err) {
   321  		klog.V(3).Infof("No file found at %q", path)
   322  		return nil
   323  	} else if err != nil {
   324  		return errors.Wrapf(err, "failed to read the symlink in %q", path)
   325  	}
   326  
   327  	if fi.Mode()&os.ModeSymlink == 0 {
   328  		return errors.Errorf("file %q is not a symlink (mode=%s)", path, fi.Mode())
   329  	}
   330  	if err := os.Remove(path); err != nil {
   331  		return errors.Wrapf(err, "failed to remove the symlink in %q", path)
   332  	}
   333  	klog.V(3).Infof("Removed symlink from %q", path)
   334  	return nil
   335  }
   336  
   337  // pluginNameToBin creates the name of the symlink file for the plugin name.
   338  // It converts dashes to underscores.
   339  func pluginNameToBin(name string, isWindows bool) string {
   340  	name = strings.ReplaceAll(name, "-", "_")
   341  	name = "kbcli-" + name
   342  	if isWindows {
   343  		name += ".exe"
   344  	}
   345  	return name
   346  }