github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/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-go/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 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 string) (Go, error) {
    52  	goProject := &goProject{version: version}
    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()
    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().GetCommonDetails().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 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  	}
   219  	buildInfoModule := buildinfo.Module{Id: module, Artifacts: artifacts, Dependencies: buildInfoDependencies}
   220  	if module == "" {
   221  		buildInfoModule.Id = project.getId()
   222  	}
   223  	return &buildinfo.BuildInfo{Modules: []buildinfo.Module{buildInfoModule}}
   224  }
   225  
   226  // Get go project ID in the form of projectName:version
   227  func (project *goProject) getId() string {
   228  	return project.moduleName
   229  }
   230  
   231  // Read go.mod file and add it as an artifact to the build info.
   232  func (project *goProject) readModFile() error {
   233  	var err error
   234  	project.projectPath, err = cmd.GetProjectRoot()
   235  	if err != nil {
   236  		return errorutils.CheckError(err)
   237  	}
   238  
   239  	modFilePath := filepath.Join(project.projectPath, "go.mod")
   240  	modFile, err := os.Open(modFilePath)
   241  	if err != nil {
   242  		return errorutils.CheckError(err)
   243  	}
   244  	defer modFile.Close()
   245  	content, err := ioutil.ReadAll(modFile)
   246  	if err != nil {
   247  		return errorutils.CheckError(err)
   248  	}
   249  
   250  	// Read module name
   251  	project.moduleName, err = parseModuleName(string(content))
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	checksums, err := checksum.Calc(bytes.NewBuffer(content))
   257  	if err != nil {
   258  		return err
   259  	}
   260  	project.modContent = content
   261  
   262  	// Add mod file as artifact
   263  	artifact := buildinfo.Artifact{Name: project.version + ".mod", Type: "mod"}
   264  	artifact.Checksum = &buildinfo.Checksum{Sha1: checksums[checksum.SHA1], Md5: checksums[checksum.MD5]}
   265  	project.artifacts = append(project.artifacts, artifact)
   266  	return nil
   267  }
   268  
   269  // Archive the go project.
   270  // Returns the path of the temp archived project file.
   271  func (project *goProject) archiveProject(version, tempDir string) (string, error) {
   272  	tempFile, err := ioutil.TempFile(tempDir, "project.zip")
   273  
   274  	if err != nil {
   275  		return "", errorutils.CheckError(err)
   276  	}
   277  	defer tempFile.Close()
   278  	err = archiveProject(tempFile, project.projectPath, project.moduleName, version)
   279  	if err != nil {
   280  		return "", errorutils.CheckError(err)
   281  	}
   282  	// Double-check that the paths within the zip file are well-formed.
   283  	fi, err := tempFile.Stat()
   284  	if err != nil {
   285  		return "", err
   286  	}
   287  	z, err := zip.NewReader(tempFile, fi.Size())
   288  	if err != nil {
   289  		return "", err
   290  	}
   291  	prefix := project.moduleName + "@" + version + "/"
   292  	for _, f := range z.File {
   293  		if !strings.HasPrefix(f.Name, prefix) {
   294  			return "", fmt.Errorf("zip for %s has unexpected file %s", prefix[:len(prefix)-1], f.Name)
   295  		}
   296  	}
   297  	// Sync the file before renaming it
   298  	if err := tempFile.Sync(); err != nil {
   299  		return "", err
   300  	}
   301  	if err := tempFile.Close(); err != nil {
   302  		return "", err
   303  	}
   304  	fileDetails, err := fileutils.GetFileDetails(tempFile.Name())
   305  	if err != nil {
   306  		return "", err
   307  	}
   308  
   309  	artifact := buildinfo.Artifact{Name: version + ".zip", Type: "zip"}
   310  	artifact.Checksum = &buildinfo.Checksum{Sha1: fileDetails.Checksum.Sha1, Md5: fileDetails.Checksum.Md5}
   311  	project.artifacts = append(project.artifacts, artifact)
   312  	return tempFile.Name(), nil
   313  }
   314  
   315  // Add the info file also as an artifact to be part of the build info.
   316  func (project *goProject) addInfoFileToBuildInfo(infoFilePath string) error {
   317  	fileDetails, err := fileutils.GetFileDetails(infoFilePath)
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	artifact := buildinfo.Artifact{Name: project.version + ".info", Type: "info"}
   323  	artifact.Checksum = &buildinfo.Checksum{Sha1: fileDetails.Checksum.Sha1, Md5: fileDetails.Checksum.Md5}
   324  	project.artifacts = append(project.artifacts, artifact)
   325  	return nil
   326  }
   327  
   328  // Parse module name from go.mod content.
   329  func parseModuleName(modContent string) (string, error) {
   330  	r, err := regexp.Compile(`module "?([\w\.@:%_\+-.~#?&]+/?.+\w)`)
   331  	if err != nil {
   332  		return "", errorutils.CheckError(err)
   333  	}
   334  	lines := strings.Split(modContent, "\n")
   335  	for _, v := range lines {
   336  		matches := r.FindStringSubmatch(v)
   337  		if len(matches) == 2 {
   338  			return matches[1], nil
   339  		}
   340  	}
   341  
   342  	return "", errorutils.CheckError(errors.New("Module name missing in go.mod file"))
   343  }
   344  
   345  type goInfo struct {
   346  	Version string `json:"Version"`
   347  	Time    string `json:"Time"`
   348  }