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

     1  package golang
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/jfrog/build-info-go/build"
     8  	biutils "github.com/jfrog/build-info-go/utils"
     9  	buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build"
    10  	"github.com/jfrog/jfrog-cli-core/v2/common/project"
    11  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    13  	goutils "github.com/jfrog/jfrog-cli-core/v2/utils/golang"
    14  	"github.com/jfrog/jfrog-client-go/auth"
    15  	"github.com/jfrog/jfrog-client-go/http/httpclient"
    16  	rtutils "github.com/jfrog/jfrog-client-go/utils"
    17  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    18  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    19  	"github.com/jfrog/jfrog-client-go/utils/log"
    20  	"net/http"
    21  	"os"
    22  	"path"
    23  	"path/filepath"
    24  	"strings"
    25  )
    26  
    27  type GoCommand struct {
    28  	goArg              []string
    29  	buildConfiguration *buildUtils.BuildConfiguration
    30  	deployerParams     *project.RepositoryConfig
    31  	resolverParams     *project.RepositoryConfig
    32  	configFilePath     string
    33  	noFallback         bool
    34  }
    35  
    36  func NewGoCommand() *GoCommand {
    37  	return &GoCommand{}
    38  }
    39  
    40  func (gc *GoCommand) SetConfigFilePath(configFilePath string) *GoCommand {
    41  	gc.configFilePath = configFilePath
    42  	return gc
    43  }
    44  
    45  func (gc *GoCommand) SetResolverParams(resolverParams *project.RepositoryConfig) *GoCommand {
    46  	gc.resolverParams = resolverParams
    47  	return gc
    48  }
    49  
    50  func (gc *GoCommand) SetDeployerParams(deployerParams *project.RepositoryConfig) *GoCommand {
    51  	gc.deployerParams = deployerParams
    52  	return gc
    53  }
    54  
    55  func (gc *GoCommand) SetBuildConfiguration(buildConfiguration *buildUtils.BuildConfiguration) *GoCommand {
    56  	gc.buildConfiguration = buildConfiguration
    57  	return gc
    58  }
    59  
    60  func (gc *GoCommand) SetGoArg(goArg []string) *GoCommand {
    61  	gc.goArg = goArg
    62  	return gc
    63  }
    64  
    65  func (gc *GoCommand) CommandName() string {
    66  	return "rt_go"
    67  }
    68  
    69  func (gc *GoCommand) ServerDetails() (*config.ServerDetails, error) {
    70  	// If deployer Artifactory details exists, returns it.
    71  	if gc.deployerParams != nil && !gc.deployerParams.IsServerDetailsEmpty() {
    72  		return gc.deployerParams.ServerDetails()
    73  	}
    74  
    75  	// If resolver Artifactory details exists, returns it.
    76  	if gc.resolverParams != nil && !gc.resolverParams.IsServerDetailsEmpty() {
    77  		return gc.resolverParams.ServerDetails()
    78  	}
    79  
    80  	vConfig, err := project.ReadConfigFile(gc.configFilePath, project.YAML)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	return buildUtils.GetServerDetails(vConfig)
    85  }
    86  
    87  func (gc *GoCommand) Run() error {
    88  	// Read config file.
    89  	log.Debug("Preparing to read the config file", gc.configFilePath)
    90  	vConfig, err := project.ReadConfigFile(gc.configFilePath, project.YAML)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// Extract resolution params.
    96  	gc.resolverParams, err = project.GetRepoConfigByPrefix(gc.configFilePath, project.ProjectConfigResolverPrefix, vConfig)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	if vConfig.IsSet(project.ProjectConfigDeployerPrefix) {
   102  		// Extract deployer params.
   103  		gc.deployerParams, err = project.GetRepoConfigByPrefix(gc.configFilePath, project.ProjectConfigDeployerPrefix, vConfig)
   104  		if err != nil {
   105  			return err
   106  		}
   107  	}
   108  
   109  	// Extract build info information from the args.
   110  	gc.goArg, gc.buildConfiguration, err = buildUtils.ExtractBuildDetailsFromArgs(gc.goArg)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// Extract no-fallback flag from the args.
   116  	gc.goArg, err = gc.extractNoFallbackFromArgs()
   117  	if err != nil {
   118  		return err
   119  	}
   120  	return gc.run()
   121  }
   122  
   123  func (gc *GoCommand) extractNoFallbackFromArgs() (cleanArgs []string, err error) {
   124  	var flagIndex int
   125  	cleanArgs = append([]string(nil), gc.goArg...)
   126  
   127  	// Extract no-fallback boolean flag from the args.
   128  	flagIndex, gc.noFallback, err = coreutils.FindBooleanFlag("--no-fallback", cleanArgs)
   129  	if err != nil {
   130  		return
   131  	}
   132  
   133  	coreutils.RemoveFlagFromCommand(&cleanArgs, flagIndex, flagIndex)
   134  	return
   135  }
   136  
   137  func (gc *GoCommand) run() (err error) {
   138  	err = goutils.LogGoVersion()
   139  	if err != nil {
   140  		return
   141  	}
   142  	goBuildInfo, err := buildUtils.PrepareBuildPrerequisites(gc.buildConfiguration)
   143  	if err != nil {
   144  		return
   145  	}
   146  	defer func() {
   147  		if goBuildInfo != nil && err != nil {
   148  			err = errors.Join(err, goBuildInfo.Clean())
   149  		}
   150  	}()
   151  
   152  	resolverDetails, err := gc.resolverParams.ServerDetails()
   153  	if err != nil {
   154  		return
   155  	}
   156  	repoUrl, err := goutils.GetArtifactoryRemoteRepoUrl(resolverDetails, gc.resolverParams.TargetRepo())
   157  	if err != nil {
   158  		return
   159  	}
   160  	// If noFallback=false, missing packages will be fetched directly from VCS
   161  	if !gc.noFallback {
   162  		repoUrl += "|direct"
   163  	}
   164  	err = biutils.RunGo(gc.goArg, repoUrl)
   165  	if errorutils.CheckError(err) != nil {
   166  		err = coreutils.ConvertExitCodeError(err)
   167  		return
   168  	}
   169  
   170  	if goBuildInfo != nil {
   171  		// Need to collect build info
   172  		tempDirPath := ""
   173  		if isGoGetCommand := len(gc.goArg) > 0 && gc.goArg[0] == "get"; isGoGetCommand {
   174  			if len(gc.goArg) < 2 {
   175  				// Package name was not supplied. Invalid go get commend
   176  				err = errorutils.CheckErrorf("Invalid get command. Package name is missing")
   177  				return
   178  			}
   179  			tempDirPath, err = fileutils.CreateTempDir()
   180  			if err != nil {
   181  				return
   182  			}
   183  			// Cleanup the temp working directory at the end.
   184  			defer func() {
   185  				err = errors.Join(err, fileutils.RemoveTempDir(tempDirPath))
   186  			}()
   187  			var serverDetails auth.ServiceDetails
   188  			serverDetails, err = resolverDetails.CreateArtAuthConfig()
   189  			if err != nil {
   190  				return
   191  			}
   192  			err = copyGoPackageFiles(tempDirPath, gc.goArg[1], gc.resolverParams.TargetRepo(), serverDetails)
   193  			if err != nil {
   194  				return
   195  			}
   196  		}
   197  		var goModule *build.GoModule
   198  		goModule, err = goBuildInfo.AddGoModule(tempDirPath)
   199  		if errorutils.CheckError(err) != nil {
   200  			return
   201  		}
   202  		if gc.buildConfiguration.GetModule() != "" {
   203  			goModule.SetName(gc.buildConfiguration.GetModule())
   204  		}
   205  		err = errorutils.CheckError(goModule.CalcDependencies())
   206  	}
   207  
   208  	return
   209  }
   210  
   211  // copyGoPackageFiles copies the package files from the go mod cache directory to the given destPath.
   212  // The path to those cache files is retrieved using the supplied package name and Artifactory details.
   213  func copyGoPackageFiles(destPath, packageName, rtTargetRepo string, authArtDetails auth.ServiceDetails) error {
   214  	packageFilesPath, err := getPackageFilePathFromArtifactory(packageName, rtTargetRepo, authArtDetails)
   215  	if err != nil {
   216  		return err
   217  	}
   218  	// Copy the entire content of the relevant Go pkg directory to the requested destination path.
   219  	err = biutils.CopyDir(packageFilesPath, destPath, true, nil)
   220  	if err != nil {
   221  		return fmt.Errorf("couldn't find suitable package files: %s", packageFilesPath)
   222  	}
   223  	// Set permission recursively
   224  	return coreutils.SetPermissionsRecursively(destPath, 0755)
   225  }
   226  
   227  // getPackageFilePathFromArtifactory returns a string that represents the package files cache path.
   228  // In most cases the path to those cache files is retrieved using the supplied package name and Artifactory details.
   229  // However, if the user asked for a specific version (package@vX.Y.Z) the unnecessary call to Artifactory is avoided.
   230  func getPackageFilePathFromArtifactory(packageName, rtTargetRepo string, authArtDetails auth.ServiceDetails) (packageFilesPath string, err error) {
   231  	var version string
   232  	packageCachePath, err := biutils.GetGoModCachePath()
   233  	if errorutils.CheckError(err) != nil {
   234  		return
   235  	}
   236  	packageNameSplitted := strings.Split(packageName, "@")
   237  	name := packageNameSplitted[0]
   238  	// The case the user asks for a specific version
   239  	if len(packageNameSplitted) == 2 && strings.HasPrefix(packageNameSplitted[1], "v") {
   240  		version = packageNameSplitted[1]
   241  	} else {
   242  		branchName := ""
   243  		// The case the user asks for a specific branch
   244  		if len(packageNameSplitted) == 2 {
   245  			branchName = packageNameSplitted[1]
   246  		}
   247  		packageVersionRequest := buildPackageVersionRequest(name, branchName)
   248  		// Retrieve the package version using Artifactory
   249  		version, err = getPackageVersion(rtTargetRepo, packageVersionRequest, authArtDetails)
   250  		if err != nil {
   251  			return
   252  		}
   253  	}
   254  	packageFilesPath, err = getFileSystemPackagePath(packageCachePath, name, version)
   255  	return
   256  
   257  }
   258  
   259  // getPackageVersion returns the matching version for the packageName string using the Artifactory details that are provided.
   260  // PackageName string should be in the following format: <Package Path>/@V/<Requested Branch Name>.info OR latest.info
   261  // For example the jfrog/jfrog-cli/@v/master.info packageName will return the corresponding canonical version (vX.Y.Z) string for the jfrog-cli master branch.
   262  func getPackageVersion(repoName, packageName string, details auth.ServiceDetails) (string, error) {
   263  	artifactoryApiUrl, err := rtutils.BuildUrl(details.GetUrl(), "api/go/"+repoName, make(map[string]string))
   264  	if err != nil {
   265  		return "", err
   266  	}
   267  	artHttpDetails := details.CreateHttpClientDetails()
   268  	client, err := httpclient.ClientBuilder().Build()
   269  	if err != nil {
   270  		return "", err
   271  	}
   272  	artifactoryApiUrl = artifactoryApiUrl + "/" + packageName
   273  	resp, body, _, err := client.SendGet(artifactoryApiUrl, true, artHttpDetails, "")
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  	if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK); err != nil {
   278  		return "", err
   279  	}
   280  	// Extract version from response
   281  	var version PackageVersionResponseContent
   282  	if err = json.Unmarshal(body, &version); err != nil {
   283  		return "", errorutils.CheckError(err)
   284  	}
   285  	return version.Version, nil
   286  }
   287  
   288  type PackageVersionResponseContent struct {
   289  	Version string `json:"Version,omitempty"`
   290  }
   291  
   292  // getFileSystemPackagePath returns a string that represents the package files cache path.
   293  // In some cases when the path isn't represented by the package name, instead the name represents a specific project's directory's path.
   294  // In this case we will scan the path until we find the package directory.
   295  // Example : When running 'go get github.com/golang/mock/mockgen@v1.4.1'
   296  //   - "mockgen" is a directory inside "mock" package ("mockgen" doesn't contain "go.mod").
   297  //   - go download and save the whole "mock" package in local cache under 'github.com/golang/mock@v1.4.1' -- >
   298  //     "go get" downloads and saves the whole "mock" package in the local cache under 'github.com/golang/mock@v1.4.1'
   299  func getFileSystemPackagePath(packageCachePath, name, version string) (string, error) {
   300  	separator := string(filepath.Separator)
   301  	// For Windows OS
   302  	path := filepath.Clean(name)
   303  	for path != "" {
   304  		packagePath := filepath.Join(packageCachePath, path+"@"+version)
   305  		exists, err := fileutils.IsDirExists(packagePath, false)
   306  		if err != nil {
   307  			return "", err
   308  		}
   309  		if exists {
   310  			return packagePath, nil
   311  		}
   312  		// Remove path's last element and check again
   313  		path, _ = filepath.Split(path)
   314  		path = strings.TrimSuffix(path, separator)
   315  	}
   316  	return "", errors.New("Could not find package: " + name + " in: " + packageCachePath)
   317  }
   318  
   319  // buildPackageVersionRequest returns a string representing the version request to Artifactory.
   320  // The resulted string is in the following format: "<Package Name>/@V/<Branch Name>.info".
   321  // If a branch name is not given, the branch name will be replaced with the "latest" keyword.
   322  // ("<Package Name>/@V/latest.info").
   323  func buildPackageVersionRequest(name, branchName string) string {
   324  	packageVersionRequest := path.Join(name, "@v")
   325  	if branchName != "" {
   326  		// A branch name was given by the user
   327  		return path.Join(packageVersionRequest, branchName+".info")
   328  	}
   329  	// No version was given to "go get" command, so the latest version should be requested
   330  	return path.Join(packageVersionRequest, "latest.info")
   331  }
   332  
   333  func SetArtifactoryAsResolutionServer(serverDetails *config.ServerDetails, depsRepo string) (err error) {
   334  	err = setGoProxy(serverDetails, depsRepo)
   335  	if err != nil {
   336  		err = fmt.Errorf("failed while setting Artifactory as a dependencies resolution registry: %s", err.Error())
   337  	}
   338  	return
   339  }
   340  
   341  func setGoProxy(server *config.ServerDetails, remoteGoRepo string) error {
   342  	repoUrl, err := goutils.GetArtifactoryRemoteRepoUrl(server, remoteGoRepo)
   343  	if err != nil {
   344  		return err
   345  	}
   346  	repoUrl += "|direct"
   347  	return os.Setenv("GOPROXY", repoUrl)
   348  }