github.com/jfrog/jfrog-cli-core@v1.12.1/artifactory/utils/golang/project/project.go (about)

     1  package project
     2  
     3  import (
     4  	"archive/zip"
     5  	"bytes"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/jfrog/gocmd/cmd"
    17  	"github.com/jfrog/gocmd/executers"
    18  	executersutils "github.com/jfrog/gocmd/executers/utils"
    19  	"github.com/jfrog/jfrog-cli-core/artifactory/utils"
    20  	"github.com/jfrog/jfrog-client-go/artifactory"
    21  	"github.com/jfrog/jfrog-client-go/artifactory/buildinfo"
    22  	_go "github.com/jfrog/jfrog-client-go/artifactory/services/go"
    23  	servicesutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    24  	cliutils "github.com/jfrog/jfrog-client-go/utils"
    25  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    26  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    27  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils/checksum"
    28  	"github.com/jfrog/jfrog-client-go/utils/log"
    29  	"github.com/jfrog/jfrog-client-go/utils/version"
    30  )
    31  
    32  // Represent go project
    33  type Go interface {
    34  	Dependencies() []executers.Package
    35  	CreateBuildInfoDependencies(includeInfoFiles bool) error
    36  	PublishPackage(targetRepo, buildName, buildNumber, projectKey string, servicesManager artifactory.ArtifactoryServicesManager) (*servicesutils.OperationSummary, error)
    37  	PublishDependencies(targetRepo string, servicesManager artifactory.ArtifactoryServicesManager, includeDepSlice []string) (succeeded, failed int, err error)
    38  	BuildInfo(includeArtifacts bool, module, targetRepository string) *buildinfo.BuildInfo
    39  	LoadDependencies() error
    40  }
    41  
    42  type goProject struct {
    43  	dependencies []executers.Package
    44  	artifacts    []buildinfo.Artifact
    45  	modContent   []byte
    46  	moduleName   string
    47  	version      string
    48  	projectPath  string
    49  }
    50  
    51  // Load go project.
    52  func Load(version, projectPath string) (Go, error) {
    53  	goProject := &goProject{version: version, projectPath: projectPath}
    54  	err := goProject.readModFile()
    55  	return goProject, err
    56  }
    57  
    58  // Get the go project dependencies.
    59  func (project *goProject) Dependencies() []executers.Package {
    60  	return project.dependencies
    61  }
    62  
    63  // Get the go project dependencies.
    64  func (project *goProject) CreateBuildInfoDependencies(includeInfoFiles bool) error {
    65  	for i, dep := range project.dependencies {
    66  		err := dep.CreateBuildInfoDependencies(includeInfoFiles)
    67  		if err != nil {
    68  			return err
    69  		}
    70  		project.dependencies[i] = dep
    71  	}
    72  	return nil
    73  }
    74  
    75  // Get the project dependencies.
    76  func (project *goProject) LoadDependencies() error {
    77  	var err error
    78  	project.dependencies, err = project.loadDependencies()
    79  	return err
    80  }
    81  
    82  func (project *goProject) loadDependencies() ([]executers.Package, error) {
    83  	cachePath, err := executersutils.GetCachePath()
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	modulesMap, err := cmd.GetDependenciesList(project.projectPath)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	if modulesMap == nil {
    92  		return nil, nil
    93  	}
    94  	return executers.GetDependencies(cachePath, modulesMap)
    95  }
    96  
    97  // Publish go project to Artifactory.
    98  func (project *goProject) PublishPackage(targetRepo, buildName, buildNumber, projectKey string, servicesManager artifactory.ArtifactoryServicesManager) (*servicesutils.OperationSummary, error) {
    99  	log.Info("Publishing", project.getId(), "to", targetRepo)
   100  
   101  	props, err := utils.CreateBuildProperties(buildName, buildNumber, projectKey)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// Temp directory for the project archive.
   107  	// The directory will be deleted at the end.
   108  	tempDirPath, err := fileutils.CreateTempDir()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	defer fileutils.RemoveTempDir(tempDirPath)
   113  
   114  	params := _go.NewGoParams()
   115  	params.Version = project.version
   116  	params.Props = props
   117  	params.TargetRepo = targetRepo
   118  	params.ModuleId = project.getId()
   119  	params.ModContent = project.modContent
   120  	params.ModPath = filepath.Join(project.projectPath, "go.mod")
   121  	params.ZipPath, err = project.archiveProject(project.version, tempDirPath)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	// Create the info file if Artifactory version is 6.10.0 and above.
   126  	artifactoryVersion, err := servicesManager.GetConfig().GetServiceDetails().GetVersion()
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	version := version.NewVersion(artifactoryVersion)
   131  	if version.AtLeast(_go.ArtifactoryMinSupportedVersionForInfoFile) {
   132  		pathToInfo, err := project.createInfoFile()
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		defer os.Remove(pathToInfo)
   137  		if len(buildName) > 0 && len(buildNumber) > 0 {
   138  			err = project.addInfoFileToBuildInfo(pathToInfo)
   139  			if err != nil {
   140  				return nil, err
   141  			}
   142  		}
   143  		params.InfoPath = pathToInfo
   144  	}
   145  
   146  	return servicesManager.PublishGoProject(params)
   147  }
   148  
   149  // Creates the info file.
   150  // Returns the path to that file.
   151  func (project *goProject) createInfoFile() (string, error) {
   152  	log.Debug("Creating info file", project.projectPath)
   153  	currentTime := time.Now().Format("2006-01-02T15:04:05Z")
   154  	goInfoContent := goInfo{Version: project.version, Time: currentTime}
   155  	content, err := json.Marshal(&goInfoContent)
   156  	if err != nil {
   157  		return "", errorutils.CheckError(err)
   158  	}
   159  	file, err := os.Create(project.version + ".info")
   160  	if err != nil {
   161  		return "", errorutils.CheckError(err)
   162  	}
   163  	defer file.Close()
   164  	_, err = file.Write(content)
   165  	if err != nil {
   166  		return "", errorutils.CheckError(err)
   167  	}
   168  	path, err := filepath.Abs(file.Name())
   169  	if err != nil {
   170  		return "", errorutils.CheckError(err)
   171  	}
   172  	log.Debug("Info file was successfully created:", path)
   173  	return path, nil
   174  }
   175  
   176  func (project *goProject) PublishDependencies(targetRepo string, servicesManager artifactory.ArtifactoryServicesManager, includeDepSlice []string) (succeeded, failed int, err error) {
   177  	log.Info("Publishing package dependencies...")
   178  	includeDep := cliutils.ConvertSliceToMap(includeDepSlice)
   179  
   180  	skip := 0
   181  	_, includeAll := includeDep["ALL"]
   182  	dependencies := project.Dependencies()
   183  	for _, dependency := range dependencies {
   184  		includeDependency := includeAll
   185  		if !includeDependency {
   186  			if _, included := includeDep[dependency.GetId()]; included {
   187  				includeDependency = true
   188  			}
   189  		}
   190  		if includeDependency {
   191  			err = dependency.Publish("", targetRepo, servicesManager)
   192  			if err != nil {
   193  				err = errors.New("Failed to publish " + dependency.GetId() + " due to: " + err.Error())
   194  				log.Error("Failed to publish", dependency.GetId(), ":", err)
   195  			} else {
   196  				succeeded++
   197  			}
   198  			continue
   199  		}
   200  		skip++
   201  	}
   202  
   203  	failed = len(dependencies) - succeeded - skip
   204  	if failed > 0 {
   205  		err = errors.New("Publishing project dependencies finished with errors. Please review the logs.")
   206  	}
   207  	return succeeded, failed, err
   208  }
   209  
   210  // Get the build info of the go project
   211  func (project *goProject) BuildInfo(includeArtifacts bool, module, targetRepository string) *buildinfo.BuildInfo {
   212  	buildInfoDependencies := []buildinfo.Dependency{}
   213  	for _, dep := range project.dependencies {
   214  		buildInfoDependencies = append(buildInfoDependencies, dep.Dependencies()...)
   215  	}
   216  	var artifacts []buildinfo.Artifact
   217  	if includeArtifacts {
   218  		artifacts = project.artifacts
   219  		// Add artifacts target-path.
   220  		moduleId := strings.Split(project.moduleName, ":")
   221  		for i := range artifacts {
   222  			artifact := &artifacts[i]
   223  			targetPath := targetRepository
   224  			_ = _go.CreateUrlPath(moduleId[0], project.version, "", "."+artifact.Type, &targetPath)
   225  			artifact.Path = targetPath
   226  		}
   227  	}
   228  	buildInfoModule := buildinfo.Module{Id: module, Type: buildinfo.Go, Artifacts: artifacts, Dependencies: buildInfoDependencies}
   229  	if module == "" {
   230  		buildInfoModule.Id = project.getId()
   231  	}
   232  	return &buildinfo.BuildInfo{Modules: []buildinfo.Module{buildInfoModule}}
   233  }
   234  
   235  // Get go project ID in the form of projectName:version
   236  func (project *goProject) getId() string {
   237  	return project.moduleName
   238  }
   239  
   240  // Read go.mod file and add it as an artifact to the build info.
   241  func (project *goProject) readModFile() error {
   242  	var err error
   243  	if project.projectPath == "" {
   244  		project.projectPath, err = cmd.GetProjectRoot()
   245  		if err != nil {
   246  			return errorutils.CheckError(err)
   247  		}
   248  	}
   249  
   250  	modFilePath := filepath.Join(project.projectPath, "go.mod")
   251  	modFileExists, _ := fileutils.IsFileExists(modFilePath, true)
   252  	if !modFileExists {
   253  		return errorutils.CheckError(errors.New("Could not find project's go.mod in " + project.projectPath))
   254  	}
   255  	modFile, err := os.Open(modFilePath)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	defer modFile.Close()
   260  	content, err := io.ReadAll(modFile)
   261  	if err != nil {
   262  		return errorutils.CheckError(err)
   263  	}
   264  
   265  	// Read module name
   266  	project.moduleName, err = parseModuleName(string(content))
   267  	if err != nil {
   268  		return err
   269  	}
   270  
   271  	checksums, err := checksum.Calc(bytes.NewBuffer(content))
   272  	if err != nil {
   273  		return err
   274  	}
   275  	project.modContent = content
   276  
   277  	// Add mod file as artifact
   278  	artifact := buildinfo.Artifact{Name: project.version + ".mod", Type: "mod"}
   279  	artifact.Checksum = buildinfo.Checksum{Sha1: checksums[checksum.SHA1], Md5: checksums[checksum.MD5]}
   280  	project.artifacts = append(project.artifacts, artifact)
   281  	return nil
   282  }
   283  
   284  // Archive the go project.
   285  // Returns the path of the temp archived project file.
   286  func (project *goProject) archiveProject(version, tempDir string) (string, error) {
   287  	tempFile, err := os.CreateTemp(tempDir, "project.zip")
   288  
   289  	if err != nil {
   290  		return "", errorutils.CheckError(err)
   291  	}
   292  	defer tempFile.Close()
   293  	err = archiveProject(tempFile, project.projectPath, project.moduleName, version)
   294  	if err != nil {
   295  		return "", errorutils.CheckError(err)
   296  	}
   297  	// Double-check that the paths within the zip file are well-formed.
   298  	fi, err := tempFile.Stat()
   299  	if err != nil {
   300  		return "", err
   301  	}
   302  	z, err := zip.NewReader(tempFile, fi.Size())
   303  	if err != nil {
   304  		return "", err
   305  	}
   306  	prefix := project.moduleName + "@" + version + "/"
   307  	for _, f := range z.File {
   308  		if !strings.HasPrefix(f.Name, prefix) {
   309  			return "", fmt.Errorf("zip for %s has unexpected file %s", prefix[:len(prefix)-1], f.Name)
   310  		}
   311  	}
   312  	// Sync the file before renaming it
   313  	if err := tempFile.Sync(); err != nil {
   314  		return "", err
   315  	}
   316  	if err := tempFile.Close(); err != nil {
   317  		return "", err
   318  	}
   319  	fileDetails, err := fileutils.GetFileDetails(tempFile.Name())
   320  	if err != nil {
   321  		return "", err
   322  	}
   323  
   324  	artifact := buildinfo.Artifact{Name: version + ".zip", Type: "zip"}
   325  	artifact.Checksum = buildinfo.Checksum{Sha1: fileDetails.Checksum.Sha1, Md5: fileDetails.Checksum.Md5}
   326  	project.artifacts = append(project.artifacts, artifact)
   327  	return tempFile.Name(), nil
   328  }
   329  
   330  // Add the info file also as an artifact to be part of the build info.
   331  func (project *goProject) addInfoFileToBuildInfo(infoFilePath string) error {
   332  	fileDetails, err := fileutils.GetFileDetails(infoFilePath)
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	artifact := buildinfo.Artifact{Name: project.version + ".info", Type: "info"}
   338  	artifact.Checksum = buildinfo.Checksum{Sha1: fileDetails.Checksum.Sha1, Md5: fileDetails.Checksum.Md5}
   339  	project.artifacts = append(project.artifacts, artifact)
   340  	return nil
   341  }
   342  
   343  // Parse module name from go.mod content.
   344  func parseModuleName(modContent string) (string, error) {
   345  	r, err := regexp.Compile(`module "?([\w\.@:%_\+-.~#?&]+/?.+\w)`)
   346  	if err != nil {
   347  		return "", errorutils.CheckError(err)
   348  	}
   349  	lines := strings.Split(modContent, "\n")
   350  	for _, v := range lines {
   351  		matches := r.FindStringSubmatch(v)
   352  		if len(matches) == 2 {
   353  			return matches[1], nil
   354  		}
   355  	}
   356  
   357  	return "", errorutils.CheckError(errors.New("Module name missing in go.mod file"))
   358  }
   359  
   360  type goInfo struct {
   361  	Version string `json:"Version"`
   362  	Time    string `json:"Time"`
   363  }