github.com/jfrog/jfrog-cli-core/v2@v2.52.0/utils/plugins/utils.go (about)

     1  package plugins
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
     7  	"github.com/jfrog/jfrog-cli-core/v2/utils/lock"
     8  	cliLog "github.com/jfrog/jfrog-cli-core/v2/utils/log"
     9  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    10  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    11  	"github.com/jfrog/jfrog-client-go/utils/log"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  	"sync"
    16  )
    17  
    18  // Internal golang locking
    19  // In case the 2 threads in the same needs to check plugins.yml file or migrate the fileSystem files to the latest version.
    20  var mutex sync.Mutex
    21  
    22  func init() {
    23  	cliLog.SetDefaultLogger()
    24  }
    25  
    26  type PluginsV1 struct {
    27  	Version int `json:"version,omitempty"`
    28  }
    29  
    30  // CheckPluginsVersionAndConvertIfNeeded In case the latest plugin's layout version isn't match to the local plugins hierarchy at '.jfrog/plugins' -
    31  // Migrate to the latest version.
    32  func CheckPluginsVersionAndConvertIfNeeded() (err error) {
    33  	// Check if 'plugins' directory exists in .jfrog
    34  	jfrogHomeDir, err := coreutils.GetJfrogHomeDir()
    35  	if err != nil {
    36  		return
    37  	}
    38  	exists, err := fileutils.IsDirExists(filepath.Join(jfrogHomeDir, coreutils.JfrogPluginsDirName), false)
    39  	if err != nil || !exists {
    40  		return
    41  	}
    42  
    43  	return readPluginsConfigAndConvertV0tToV1IfNeeded()
    44  }
    45  
    46  func readPluginsConfigAndConvertV0tToV1IfNeeded() (err error) {
    47  	content, err := getPluginsConfigFileContent()
    48  	// Return without converting in case of an error, or if the plugins.yml file already exists, which indicates a conversion has already been made.
    49  	if err != nil || len(content) != 0 {
    50  		return
    51  	}
    52  	// Locking mechanism - two threads in the same process.
    53  	mutex.Lock()
    54  	defer mutex.Unlock()
    55  	// Locking mechanism - in case two process would read/migrate local files at '.jfrog/plugins'.
    56  	var lockDirPath string
    57  	lockDirPath, err = coreutils.GetJfrogPluginsLockDir()
    58  	if err != nil {
    59  		return
    60  	}
    61  	var unlockFunc func() error
    62  	unlockFunc, err = lock.CreateLock(lockDirPath)
    63  	// Defer the lockFile.Unlock() function before throwing a possible error to avoid deadlock situations.
    64  	defer func() {
    65  		err = errors.Join(err, unlockFunc())
    66  	}()
    67  	if err != nil {
    68  		return
    69  	}
    70  	// The reason behind reading the config again is that it's possible that another thread or process already changed the plugins file,
    71  	// So we read again inside that locked section to indicate that we indeed need to convert the plugins' layout.
    72  	content, err = getPluginsConfigFileContent()
    73  	if err != nil {
    74  		return
    75  	}
    76  	if len(content) == 0 {
    77  		// No plugins.yaml file was found. This means that we are in v0.
    78  		// Convert plugins layout to the latest version.
    79  		_, err = convertPluginsV0ToV1()
    80  	}
    81  	return
    82  }
    83  
    84  func getPluginsConfigFileContent() (content []byte, err error) {
    85  	pluginsFilePath, err := getPluginsFilePath()
    86  	if err != nil {
    87  		return
    88  	}
    89  	exists, err := fileutils.IsFileExists(pluginsFilePath, false)
    90  	if err != nil {
    91  		return
    92  	}
    93  	if exists {
    94  		content, err = fileutils.ReadFile(pluginsFilePath)
    95  	}
    96  	return
    97  }
    98  
    99  // V0: in the plugins directory there was no 'plugins.yaml' file. This means that all executable files are in the same directory.
   100  // V1: We should create a 'plugins.yml' file inside the 'plugins' directory, and also modify the files' hierarchy inside 'plugins' directory.
   101  func convertPluginsV0ToV1() (*PluginsV1, error) {
   102  	err := convertFileSystemLayoutV0ToV1()
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return CreatePluginsConfigFile()
   107  }
   108  
   109  // Change the file's hierarchy inside 'plugins' directory to:
   110  //
   111  //	plugins (dir)
   112  //		plugin-name (dir)
   113  //			bin (dir)
   114  //				plugin-executable (file)
   115  //			resources:(optional dir)
   116  //				... (directories/files)
   117  func convertFileSystemLayoutV0ToV1() error {
   118  	plugins, err := coreutils.GetPluginsDirContent()
   119  	if err != nil {
   120  		return err
   121  	}
   122  	pluginsDir, err := coreutils.GetJfrogPluginsDir()
   123  	if err != nil {
   124  		return err
   125  	}
   126  	for _, p := range plugins {
   127  		// Skip 'plugins.yaml'
   128  		if p.Name() == coreutils.JfrogPluginsFileName {
   129  			continue
   130  		}
   131  		if p.IsDir() {
   132  			log.Error("unexpected directory in plugins directory: " + p.Name())
   133  			continue
   134  		}
   135  
   136  		pluginsName := removeFileExtension(p.Name())
   137  		// For example case of ".DS_Store" files
   138  		if pluginsName == "" {
   139  			continue
   140  		}
   141  		// Move plugins exec files inside a directory, which has the plugin's name.
   142  		// Create a directory with the plugin's name + "_dir" extension, move the file inside and change directory's name back to plugin's name only.
   143  		pluginDirPathWithExtension := filepath.Join(pluginsDir, pluginsName+"_dir")
   144  		err = os.MkdirAll(filepath.Join(pluginDirPathWithExtension, coreutils.PluginsExecDirName), 0777)
   145  		if err != nil {
   146  			return errorutils.CheckError(err)
   147  		}
   148  		err = fileutils.MoveFile(filepath.Join(pluginsDir, p.Name()), filepath.Join(pluginDirPathWithExtension, coreutils.PluginsExecDirName, p.Name()))
   149  		if err != nil {
   150  			return err
   151  		}
   152  		err = fileutils.MoveDir(pluginDirPathWithExtension, filepath.Join(pluginsDir, pluginsName))
   153  		if err != nil {
   154  			return err
   155  		}
   156  		err = os.RemoveAll(pluginDirPathWithExtension)
   157  		if err != nil {
   158  			return errorutils.CheckError(err)
   159  		}
   160  		err = coreutils.ChmodPluginsDirectoryContent()
   161  		if err != nil {
   162  			return err
   163  		}
   164  	}
   165  	return nil
   166  }
   167  
   168  func removeFileExtension(fileName string) string {
   169  	return strings.Split(fileName, ".")[0]
   170  }
   171  
   172  func CreatePluginsConfigFile() (*PluginsV1, error) {
   173  	pluginsFilePath, err := getPluginsFilePath()
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	plugins := new(PluginsV1)
   178  	plugins.Version = 1
   179  	content, err := json.Marshal(plugins)
   180  	if err != nil {
   181  		return nil, errorutils.CheckError(err)
   182  	}
   183  	err = os.WriteFile(pluginsFilePath, content, 0600)
   184  	if err != nil {
   185  		return nil, errorutils.CheckError(err)
   186  	}
   187  	return plugins, nil
   188  }
   189  
   190  func getPluginsFilePath() (string, error) {
   191  	pluginsFilePath, err := coreutils.GetJfrogHomeDir()
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  	err = os.MkdirAll(pluginsFilePath, 0777)
   196  	if err != nil {
   197  		return "", err
   198  	}
   199  	pluginsFilePath = filepath.Join(pluginsFilePath, coreutils.JfrogPluginsDirName, coreutils.JfrogPluginsFileName)
   200  	return pluginsFilePath, nil
   201  }
   202  
   203  func GetLocalPluginExecutableName(pluginName string) string {
   204  	if coreutils.IsWindows() {
   205  		return pluginName + ".exe"
   206  	}
   207  	return pluginName
   208  }