github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferinstall/datatransferinstall.go (about)

     1  package transferinstall
     2  
     3  import (
     4  	"fmt"
     5  	biutils "github.com/jfrog/build-info-go/utils"
     6  	"github.com/jfrog/gofrog/version"
     7  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
     8  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
     9  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    10  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    11  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    12  	"github.com/jfrog/jfrog-client-go/utils/log"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"strings"
    19  )
    20  
    21  const (
    22  	pluginName          = "data-transfer"
    23  	groovyFileName      = "dataTransfer.groovy"
    24  	jarFileName         = "data-transfer.jar"
    25  	dataTransferUrl     = "https://releases.jfrog.io/artifactory/jfrog-releases/data-transfer"
    26  	libDir              = "lib"
    27  	artifactory         = "artifactory"
    28  	pluginReloadRestApi = "api/plugins/reload"
    29  	jfrogHomeEnvVar     = "JFROG_HOME"
    30  	latest              = "[RELEASE]"
    31  )
    32  
    33  var (
    34  	defaultSearchPath = filepath.Join("opt", "jfrog")
    35  	// Plugin directory locations
    36  	originalDirPath = FileItem{"etc", "plugins"}
    37  	v7DirPath       = FileItem{"var", "etc", "artifactory", "plugins"}
    38  	// Error types
    39  	notValidDestinationErr = fmt.Errorf("can't find the directory in which to install the data-transfer plugin. Please ensure you're running this command on the machine on which Artifactory is installed. You can also use the --home-dir option to specify the directory.")
    40  	downloadConnectionErr  = func(baseUrl, fileName, err string) error {
    41  		return fmt.Errorf("Could not download the plugin file - '%s' from '%s' due to the following error: '%s'. If this machine has no network access to the download URL, you can download these files from another machine and place them in a directory on this machine. You can then run this command again with the --dir command option, with the directory containing the files as the value.", fileName, baseUrl, err)
    42  	}
    43  	// Plugin files
    44  	transferPluginFiles = PluginFiles{
    45  		FileItem{groovyFileName},
    46  		FileItem{libDir, jarFileName},
    47  	}
    48  )
    49  
    50  type InstallDataTransferPluginCommand struct {
    51  	// The server that the plugin will be installed on
    52  	targetServer *config.ServerDetails
    53  	// install manager manages the list of all the plugin files and the optional target destinations for plugin directory
    54  	transferManger *PluginInstallManager
    55  	// The plugin version to download
    56  	installVersion *version.Version
    57  	// The local directory the plugin files will be copied from
    58  	localPluginFilesDir string
    59  	// The JFrog home directory path provided from cliutils.InstallPluginHomeDir flag
    60  	localJFrogHomePath string
    61  }
    62  
    63  func (idtp *InstallDataTransferPluginCommand) CommandName() string {
    64  	return "rt_transfer_install"
    65  }
    66  
    67  func (idtp *InstallDataTransferPluginCommand) ServerDetails() (*config.ServerDetails, error) {
    68  	return idtp.targetServer, nil
    69  }
    70  
    71  // Set the local file system directory path that the plugin files will be copied from
    72  func (idtp *InstallDataTransferPluginCommand) SetLocalPluginFiles(localDir string) *InstallDataTransferPluginCommand {
    73  	idtp.localPluginFilesDir = localDir
    74  	return idtp
    75  }
    76  
    77  // Set the plugin version we want to download
    78  func (idtp *InstallDataTransferPluginCommand) SetInstallVersion(installVersion *version.Version) *InstallDataTransferPluginCommand {
    79  	idtp.installVersion = installVersion
    80  	return idtp
    81  }
    82  
    83  // Set the Jfrog home directory path
    84  func (idtp *InstallDataTransferPluginCommand) SetJFrogHomePath(path string) *InstallDataTransferPluginCommand {
    85  	idtp.localJFrogHomePath = path
    86  	return idtp
    87  }
    88  
    89  // Create InstallDataTransferCommand
    90  func NewInstallDataTransferCommand(server *config.ServerDetails) *InstallDataTransferPluginCommand {
    91  	manager := NewArtifactoryPluginInstallManager(transferPluginFiles)
    92  	cmd := &InstallDataTransferPluginCommand{
    93  		targetServer:   server,
    94  		transferManger: manager,
    95  		// Latest
    96  		installVersion:      nil,
    97  		localPluginFilesDir: "",
    98  		localJFrogHomePath:  "",
    99  	}
   100  	return cmd
   101  }
   102  
   103  // PluginInstallManager holds all the plugin files and optional plugin directory destinations.
   104  // We construct the destination path by searching for the existing plugin directory destination from the options
   105  // and joining the local path of the plugin file with the plugin directory
   106  type PluginInstallManager struct {
   107  	// All the plugin files, represented as FileItem with path starting from inside the plugin directory
   108  	// i.e. for file plugins/lib/file we represent them as FileItem{"lib","file"}
   109  	files PluginFiles
   110  	// All the optional destinations of the plugin directory starting from the JFrog home directory represented as FileItem
   111  	destinations []FileItem
   112  }
   113  
   114  // Creates a new Artifactory plugin installation manager
   115  func NewArtifactoryPluginInstallManager(bundle PluginFiles) *PluginInstallManager {
   116  	manager := &PluginInstallManager{
   117  		files:        bundle,
   118  		destinations: []FileItem{},
   119  	}
   120  	// Add all the optional destinations for the plugin dir
   121  	manager.addDestination(originalDirPath)
   122  	manager.addDestination(v7DirPath)
   123  	return manager
   124  }
   125  
   126  // Add optional plugin directory location as destination
   127  func (ftm *PluginInstallManager) addDestination(directory FileItem) {
   128  	ftm.destinations = append(ftm.destinations, directory)
   129  }
   130  
   131  // Search all the local target directories that the plugin directory can exist in base on a given root JFrog Artifactory home directory
   132  // We are searching by Joining rootDir/optionalDestination to find the right structure of the JFrog home directory.
   133  // The first option that matched and the directory exists is returned as target.
   134  func (ftm *PluginInstallManager) findDestination(rootDir string) (exists bool, destination FileItem, err error) {
   135  	if exists, err = fileutils.IsDirExists(rootDir, false); err != nil || !exists {
   136  		return
   137  	}
   138  	exists = false
   139  	// Search for the product Artifactory folder
   140  	folderItems, err := fileutils.ListFiles(rootDir, true)
   141  	if err != nil {
   142  		return
   143  	}
   144  	for _, folder := range folderItems {
   145  		if strings.Contains(folder, artifactory) {
   146  			// Search for the destination inside the folder
   147  			for _, optionalPluginDirDst := range ftm.destinations {
   148  				if exists, err = fileutils.IsDirExists(optionalPluginDirDst.toPath(folder), false); err != nil {
   149  					return
   150  				}
   151  				if exists {
   152  					destination = append([]string{folder}, optionalPluginDirDst...)
   153  					return
   154  				}
   155  			}
   156  		}
   157  	}
   158  	return
   159  }
   160  
   161  // Represents a path to file/directory
   162  // We represent file 'dir/dir2/file' as FileItem{"dir", "dir2", "file"}
   163  // We represent directory 'dir/dir2/' as FileItem{"dir", "dir2"}
   164  type FileItem []string
   165  
   166  // List of files represented as FileItem
   167  type PluginFiles []FileItem
   168  
   169  // Get the name (last) componenet of the item
   170  // i.e. for FileItem{"root", "", "dir", "file.ext"} -> "file.ext"
   171  func (f *FileItem) Name() string {
   172  	size := len(*f)
   173  	if size == 0 {
   174  		return ""
   175  	}
   176  	return (*f)[size-1]
   177  }
   178  
   179  // Get the directory FileItem representation of the item, removing the last componenet and ignoring empty entries.
   180  // i.e. for FileItem{"root", "", "dir", "file.ext"} -> FileItem{"root", "dir"}
   181  func (f *FileItem) Dirs() *FileItem {
   182  	dirs := FileItem{}
   183  	for i := 0; i < len(*f)-1; i++ {
   184  		dir := (*f)[i]
   185  		if dir != "" {
   186  			dirs = append(dirs, dir)
   187  		}
   188  	}
   189  	return &dirs
   190  }
   191  
   192  // Split and get the name and directory componenets of the item
   193  func (f *FileItem) SplitNameAndDirs() (string, *FileItem) {
   194  	return f.Name(), f.Dirs()
   195  }
   196  
   197  // Convert the item to URL representation, adding prefix tokens as provided
   198  func (f *FileItem) toURL(prefixUrl string) (string, error) {
   199  	myUrl, err := url.Parse(prefixUrl)
   200  	if err != nil {
   201  		return "", errorutils.CheckError(err)
   202  	}
   203  	myUrl.Path = path.Join(myUrl.Path, path.Join(*f...))
   204  	return myUrl.String(), nil
   205  }
   206  
   207  // Convert the item to file path representation, ignoring empty entries, adding prefix tokens as provided
   208  func (f *FileItem) toPath(previousTokens ...string) string {
   209  	return filepath.Join(filepath.Join(previousTokens...), filepath.Join(*f.Dirs()...), f.Name())
   210  }
   211  
   212  // Get the plugin directory destination to install the plugin in it. The search is starting from JFrog home directory
   213  func (idtp *InstallDataTransferPluginCommand) getPluginDirDestination() (target FileItem, err error) {
   214  	var exists bool
   215  	var envVal string
   216  
   217  	// Flag override
   218  	if idtp.localJFrogHomePath != "" {
   219  		jfrogHomeDir := strings.TrimSpace(idtp.localJFrogHomePath)
   220  		log.Debug(fmt.Sprintf("Searching for the 'plugins' directory in the JFrog home directory '%s'.", jfrogHomeDir))
   221  		if exists, target, err = idtp.transferManger.findDestination(jfrogHomeDir); err != nil || exists {
   222  			return
   223  		}
   224  		if !exists {
   225  			err = notValidDestinationErr
   226  			return
   227  		}
   228  	}
   229  	// Environment variable override
   230  	if envVal, exists = os.LookupEnv(jfrogHomeEnvVar); exists {
   231  		jfrogHomeDir := strings.TrimSpace(envVal)
   232  		log.Debug(fmt.Sprintf("Searching for the 'plugins' directory in the JFrog home directory '%s' retrieved from the '%s' environment variable.", jfrogHomeDir, jfrogHomeEnvVar))
   233  		if exists, target, err = idtp.transferManger.findDestination(jfrogHomeDir); err != nil || exists {
   234  			return
   235  		}
   236  	}
   237  	// Default value
   238  	if !coreutils.IsWindows() {
   239  		log.Debug(fmt.Sprintf("Searching for the 'plugins' directory in the default path '%s'.", defaultSearchPath))
   240  		if exists, target, err = idtp.transferManger.findDestination(defaultSearchPath); err != nil || exists {
   241  			return
   242  		}
   243  	}
   244  
   245  	err = notValidDestinationErr
   246  	return
   247  }
   248  
   249  // Transfers the file bundle from src to dst
   250  type TransferAction func(src string, dst string, bundle PluginFiles) error
   251  
   252  // Returns the src path and a transfer action
   253  func (idtp *InstallDataTransferPluginCommand) getTransferSourceAndAction() (src string, transferAction TransferAction, err error) {
   254  	// Check if local directory was provided
   255  	if idtp.localPluginFilesDir != "" {
   256  		src = idtp.localPluginFilesDir
   257  		transferAction = CopyFiles
   258  		log.Debug("Local plugin files provided, copying from file system.")
   259  		return
   260  	}
   261  	// Download files from web
   262  	var baseSrc *url.URL
   263  	if baseSrc, err = url.Parse(dataTransferUrl); err != nil {
   264  		return
   265  	}
   266  	if idtp.installVersion == nil {
   267  		// Latest
   268  		baseSrc.Path = path.Join(baseSrc.Path, latest)
   269  		log.Debug("Fetching latest version to the target.")
   270  	} else {
   271  		baseSrc.Path = path.Join(baseSrc.Path, idtp.installVersion.GetVersion())
   272  		log.Debug(fmt.Sprintf("Fetching plugin version '%s' to the target.", idtp.installVersion.GetVersion()))
   273  	}
   274  	src = baseSrc.String()
   275  	transferAction = DownloadFiles
   276  	return
   277  }
   278  
   279  // Download the plugin files from the given url to the target directory (create path if needed or override existing files)
   280  func DownloadFiles(src string, pluginDir string, bundle PluginFiles) (err error) {
   281  	for _, file := range bundle {
   282  		fileName, fileDirs := file.SplitNameAndDirs()
   283  		srcURL, e := file.toURL(src)
   284  		if e != nil {
   285  			return e
   286  		}
   287  		dstDirPath := fileDirs.toPath(pluginDir)
   288  		dirURL, e := fileDirs.toURL(src)
   289  		if e != nil {
   290  			return e
   291  		}
   292  		log.Debug(fmt.Sprintf("Downloading '%s' from '%s' to '%s'", fileName, dirURL, dstDirPath))
   293  		if err = fileutils.CreateDirIfNotExist(dstDirPath); err != nil {
   294  			return
   295  		}
   296  		if err = biutils.DownloadFile(filepath.Join(dstDirPath, fileName), srcURL); err != nil {
   297  			err = downloadConnectionErr(src, fileName, err.Error())
   298  			return
   299  		}
   300  	}
   301  	return
   302  }
   303  
   304  // Copy the plugin files from the given source to the target directory (create path if needed or override existing files)
   305  func CopyFiles(src string, pluginDir string, bundle PluginFiles) (err error) {
   306  	for _, file := range bundle {
   307  		fileName, fileDirs := file.SplitNameAndDirs()
   308  		srcPath := filepath.Join(src, fileName)
   309  		dstDirPath := fileDirs.toPath(pluginDir)
   310  		log.Debug(fmt.Sprintf("Copying '%s' from '%s' to '%s'", fileName, src, dstDirPath))
   311  		if err = fileutils.CreateDirIfNotExist(dstDirPath); err != nil {
   312  			return
   313  		}
   314  		if err = biutils.CopyFile(dstDirPath, srcPath); err != nil {
   315  			return
   316  		}
   317  	}
   318  	return
   319  }
   320  
   321  // Send reload request to the Artifactory in order to reload the plugin files.
   322  func (idtp *InstallDataTransferPluginCommand) sendReloadRequest() error {
   323  	serviceManager, err := utils.CreateServiceManager(idtp.targetServer, -1, 0, false)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	serviceDetails := serviceManager.GetConfig().GetServiceDetails()
   328  	httpDetails := serviceDetails.CreateHttpClientDetails()
   329  
   330  	resp, body, err := serviceManager.Client().SendPost(serviceDetails.GetUrl()+pluginReloadRestApi, []byte{}, &httpDetails)
   331  	if err != nil {
   332  		return err
   333  	}
   334  	if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil {
   335  		return err
   336  	}
   337  	return nil
   338  }
   339  
   340  func (idtp *InstallDataTransferPluginCommand) Run() (err error) {
   341  	log.Info(coreutils.PrintBoldTitle(fmt.Sprintf("Installing '%s' plugin...", pluginName)))
   342  
   343  	// Get source, destination and transfer action
   344  	dst, err := idtp.getPluginDirDestination()
   345  	if err != nil {
   346  		return
   347  	}
   348  	src, transferAction, err := idtp.getTransferSourceAndAction()
   349  	if err != nil {
   350  		return
   351  	}
   352  	// Execute transferring action
   353  	if err = transferAction(src, dst.toPath(), idtp.transferManger.files); err != nil {
   354  		return
   355  	}
   356  	// Reload plugins
   357  	if err = idtp.sendReloadRequest(); err != nil {
   358  		return
   359  	}
   360  
   361  	log.Info(coreutils.PrintBoldTitle(fmt.Sprintf("The %s plugin installed successfully.", pluginName)))
   362  	return
   363  }