github.com/osievert/jfrog-cli-core@v1.2.7/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/ioutil"
    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  	cliutils "github.com/jfrog/jfrog-client-go/utils"
    24  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    25  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    26  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils/checksum"
    27  	"github.com/jfrog/jfrog-client-go/utils/log"
    28  	"github.com/jfrog/jfrog-client-go/utils/version"
    29  )
    30  
    31  // Represent go project
    32  type Go interface {
    33  	Dependencies() []executers.Package
    34  	CreateBuildInfoDependencies(includeInfoFiles bool) error
    35  	PublishPackage(targetRepo, buildName, buildNumber string, servicesManager artifactory.ArtifactoryServicesManager) error
    36  	PublishDependencies(targetRepo string, servicesManager artifactory.ArtifactoryServicesManager, includeDepSlice []string) (succeeded, failed int, err error)
    37  	BuildInfo(includeArtifacts bool, module, targetRepository string) *buildinfo.BuildInfo
    38  	LoadDependencies() error
    39  }
    40  
    41  type goProject struct {
    42  	dependencies []executers.Package
    43  	artifacts    []buildinfo.Artifact
    44  	modContent   []byte
    45  	moduleName   string
    46  	version      string
    47  	projectPath  string
    48  }
    49  
    50  // Load go project.
    51  func Load(version, projectPath string) (Go, error) {
    52  	goProject := &goProject{version: version, projectPath: projectPath}
    53  	err := goProject.readModFile()
    54  	return goProject, err
    55  }
    56  
    57  // Get the go project dependencies.
    58  func (project *goProject) Dependencies() []executers.Package {
    59  	return project.dependencies
    60  }
    61  
    62  // Get the go project dependencies.
    63  func (project *goProject) CreateBuildInfoDependencies(includeInfoFiles bool) error {
    64  	for i, dep := range project.dependencies {
    65  		err := dep.CreateBuildInfoDependencies(includeInfoFiles)
    66  		if err != nil {
    67  			return err
    68  		}
    69  		project.dependencies[i] = dep
    70  	}
    71  	return nil
    72  }
    73  
    74  // Get the project dependencies.
    75  func (project *goProject) LoadDependencies() error {
    76  	var err error
    77  	project.dependencies, err = project.loadDependencies()
    78  	return err
    79  }
    80  
    81  func (project *goProject) loadDependencies() ([]executers.Package, error) {
    82  	cachePath, err := executersutils.GetCachePath()
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	modulesMap, err := cmd.GetDependenciesGraph(project.projectPath)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	if modulesMap == nil {
    91  		return nil, nil
    92  	}
    93  	return executers.GetDependencies(cachePath, modulesMap)
    94  }
    95  
    96  // Publish go project to Artifactory.
    97  func (project *goProject) PublishPackage(targetRepo, buildName, buildNumber string, servicesManager artifactory.ArtifactoryServicesManager) error {
    98  	log.Info("Publishing", project.getId(), "to", targetRepo)
    99  
   100  	props, err := utils.CreateBuildProperties(buildName, buildNumber)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	// Temp directory for the project archive.
   106  	// The directory will be deleted at the end.
   107  	tempDirPath, err := fileutils.CreateTempDir()
   108  	if err != nil {
   109  		return err
   110  	}
   111  	defer fileutils.RemoveTempDir(tempDirPath)
   112  
   113  	params := _go.NewGoParams()
   114  	params.Version = project.version
   115  	params.Props = props
   116  	params.TargetRepo = targetRepo
   117  	params.ModuleId = project.getId()
   118  	params.ModContent = project.modContent
   119  	params.ModPath = filepath.Join(project.projectPath, "go.mod")
   120  	params.ZipPath, err = project.archiveProject(project.version, tempDirPath)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	// Create the info file if Artifactory version is 6.10.0 and above.
   125  	artifactoryVersion, err := servicesManager.GetConfig().GetServiceDetails().GetVersion()
   126  	if err != nil {
   127  		return err
   128  	}
   129  	version := version.NewVersion(artifactoryVersion)
   130  	if version.AtLeast(_go.ArtifactoryMinSupportedVersionForInfoFile) {
   131  		pathToInfo, err := project.createInfoFile()
   132  		if err != nil {
   133  			return err
   134  		}
   135  		defer os.Remove(pathToInfo)
   136  		if len(buildName) > 0 && len(buildNumber) > 0 {
   137  			err = project.addInfoFileToBuildInfo(pathToInfo)
   138  			if err != nil {
   139  				return err
   140  			}
   141  		}
   142  		params.InfoPath = pathToInfo
   143  	}
   144  
   145  	return servicesManager.PublishGoProject(params)
   146  }
   147  
   148  // Creates the info file.
   149  // Returns the path to that file.
   150  func (project *goProject) createInfoFile() (string, error) {
   151  	log.Debug("Creating info file", project.projectPath)
   152  	currentTime := time.Now().Format("2006-01-02T15:04:05Z")
   153  	goInfoContent := goInfo{Version: project.version, Time: currentTime}
   154  	content, err := json.Marshal(&goInfoContent)
   155  	if err != nil {
   156  		return "", errorutils.CheckError(err)
   157  	}
   158  	file, err := os.Create(project.version + ".info")
   159  	if err != nil {
   160  		return "", errorutils.CheckError(err)
   161  	}
   162  	defer file.Close()
   163  	_, err = file.Write(content)
   164  	if err != nil {
   165  		return "", errorutils.CheckError(err)
   166  	}
   167  	path, err := filepath.Abs(file.Name())
   168  	if err != nil {
   169  		return "", errorutils.CheckError(err)
   170  	}
   171  	log.Debug("Info file was successfully created:", path)
   172  	return path, nil
   173  }
   174  
   175  func (project *goProject) PublishDependencies(targetRepo string, servicesManager artifactory.ArtifactoryServicesManager, includeDepSlice []string) (succeeded, failed int, err error) {
   176  	log.Info("Publishing package dependencies...")
   177  	includeDep := cliutils.ConvertSliceToMap(includeDepSlice)
   178  
   179  	skip := 0
   180  	_, includeAll := includeDep["ALL"]
   181  	dependencies := project.Dependencies()
   182  	for _, dependency := range dependencies {
   183  		includeDependency := includeAll
   184  		if !includeDependency {
   185  			if _, included := includeDep[dependency.GetId()]; included {
   186  				includeDependency = true
   187  			}
   188  		}
   189  		if includeDependency {
   190  			err = dependency.Publish("", targetRepo, servicesManager)
   191  			if err != nil {
   192  				err = errors.New("Failed to publish " + dependency.GetId() + " due to: " + err.Error())
   193  				log.Error("Failed to publish", dependency.GetId(), ":", err)
   194  			} else {
   195  				succeeded++
   196  			}
   197  			continue
   198  		}
   199  		skip++
   200  	}
   201  
   202  	failed = len(dependencies) - succeeded - skip
   203  	if failed > 0 {
   204  		err = errors.New("Publishing project dependencies finished with errors. Please review the logs.")
   205  	}
   206  	return succeeded, failed, err
   207  }
   208  
   209  // Get the build info of the go project
   210  func (project *goProject) BuildInfo(includeArtifacts bool, module, targetRepository string) *buildinfo.BuildInfo {
   211  	buildInfoDependencies := []buildinfo.Dependency{}
   212  	for _, dep := range project.dependencies {
   213  		buildInfoDependencies = append(buildInfoDependencies, dep.Dependencies()...)
   214  	}
   215  	var artifacts []buildinfo.Artifact
   216  	if includeArtifacts {
   217  		artifacts = project.artifacts
   218  		// Add artifacts target-path.
   219  		moduleId := strings.Split(project.moduleName, ":")
   220  		for i := range artifacts {
   221  			artifact := &artifacts[i]
   222  			targetPath := targetRepository
   223  			_ = _go.CreateUrlPath(moduleId[0], project.version, "", "."+artifact.Type, &targetPath)
   224  			artifact.Path = targetPath
   225  		}
   226  	}
   227  	buildInfoModule := buildinfo.Module{Id: module, Type: buildinfo.Go, Artifacts: artifacts, Dependencies: buildInfoDependencies}
   228  	if module == "" {
   229  		buildInfoModule.Id = project.getId()
   230  	}
   231  	return &buildinfo.BuildInfo{Modules: []buildinfo.Module{buildInfoModule}}
   232  }
   233  
   234  // Get go project ID in the form of projectName:version
   235  func (project *goProject) getId() string {
   236  	return project.moduleName
   237  }
   238  
   239  // Read go.mod file and add it as an artifact to the build info.
   240  func (project *goProject) readModFile() error {
   241  	var err error
   242  	if project.projectPath == "" {
   243  		project.projectPath, err = cmd.GetProjectRoot()
   244  		if err != nil {
   245  			return errorutils.CheckError(err)
   246  		}
   247  	}
   248  
   249  	modFilePath := filepath.Join(project.projectPath, "go.mod")
   250  	modFileExists, _ := fileutils.IsFileExists(modFilePath, true)
   251  	if !modFileExists {
   252  		log.Info("Could not find go.mod in ", project.projectPath)
   253  		return nil
   254  	}
   255  	modFile, err := os.Open(modFilePath)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	defer modFile.Close()
   260  	content, err := ioutil.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 := ioutil.TempFile(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  }