github.com/jfrog/jfrog-cli-core/v2@v2.51.0/common/build/buildutils.go (about)

     1  package build
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/jfrog/build-info-go/build"
    17  	buildInfo "github.com/jfrog/build-info-go/entities"
    18  	"github.com/jfrog/jfrog-cli-core/v2/common/project"
    19  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    20  	artClientUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    21  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    22  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    23  	"github.com/jfrog/jfrog-client-go/utils/log"
    24  )
    25  
    26  const (
    27  	BuildInfoDetails          = "details"
    28  	BuildTempPath             = "jfrog/builds/"
    29  	ProjectConfigBuildNameKey = "name"
    30  )
    31  
    32  func CreateBuildInfoService() *build.BuildInfoService {
    33  	buildInfoService := build.NewBuildInfoService()
    34  	buildInfoService.SetTempDirPath(filepath.Join(coreutils.GetCliPersistentTempDirPath(), BuildTempPath))
    35  	buildInfoService.SetLogger(log.Logger)
    36  	return buildInfoService
    37  }
    38  
    39  func PrepareBuildPrerequisites(buildConfiguration *BuildConfiguration) (build *build.Build, err error) {
    40  	// Prepare build-info.
    41  	toCollect, err := buildConfiguration.IsCollectBuildInfo()
    42  	if err != nil {
    43  		return
    44  	}
    45  	if toCollect {
    46  		log.Debug("Preparing build prerequisites...")
    47  		var buildName, buildNumber string
    48  		buildName, err = buildConfiguration.GetBuildName()
    49  		if err != nil {
    50  			return
    51  		}
    52  		buildNumber, err = buildConfiguration.GetBuildNumber()
    53  		if err != nil {
    54  			return
    55  		}
    56  		projectKey := buildConfiguration.GetProject()
    57  		buildInfoService := CreateBuildInfoService()
    58  		build, err = buildInfoService.GetOrCreateBuildWithProject(buildName, buildNumber, projectKey)
    59  		if err != nil {
    60  			err = errorutils.CheckError(err)
    61  		}
    62  	}
    63  
    64  	return
    65  }
    66  
    67  func GetBuildDir(buildName, buildNumber, projectKey string) (string, error) {
    68  	hash := sha256.Sum256([]byte(buildName + "_" + buildNumber + "_" + projectKey))
    69  	buildsDir := filepath.Join(coreutils.GetCliPersistentTempDirPath(), BuildTempPath, hex.EncodeToString(hash[:]))
    70  	err := os.MkdirAll(buildsDir, 0777)
    71  	if errorutils.CheckError(err) != nil {
    72  		return "", err
    73  	}
    74  	return buildsDir, nil
    75  }
    76  
    77  func CreateBuildProperties(buildName, buildNumber, projectKey string) (string, error) {
    78  	if buildName == "" || buildNumber == "" {
    79  		return "", nil
    80  	}
    81  
    82  	buildGeneralDetails, err := ReadBuildInfoGeneralDetails(buildName, buildNumber, projectKey)
    83  	if err != nil {
    84  		return fmt.Sprintf("build.name=%s;build.number=%s", buildName, buildNumber), err
    85  	}
    86  	timestamp := strconv.FormatInt(buildGeneralDetails.Timestamp.UnixNano()/int64(time.Millisecond), 10)
    87  	return fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp), nil
    88  }
    89  
    90  func getPartialsBuildDir(buildName, buildNumber, projectKey string) (string, error) {
    91  	buildDir, err := GetBuildDir(buildName, buildNumber, projectKey)
    92  	if err != nil {
    93  		return "", err
    94  	}
    95  	buildDir = filepath.Join(buildDir, "partials")
    96  	err = os.MkdirAll(buildDir, 0777)
    97  	if errorutils.CheckError(err) != nil {
    98  		return "", err
    99  	}
   100  	return buildDir, nil
   101  }
   102  
   103  func saveBuildData(action interface{}, buildName, buildNumber, projectKey string) (err error) {
   104  	b, err := json.Marshal(&action)
   105  	if errorutils.CheckError(err) != nil {
   106  		return err
   107  	}
   108  	var content bytes.Buffer
   109  	err = json.Indent(&content, b, "", "  ")
   110  	if errorutils.CheckError(err) != nil {
   111  		return err
   112  	}
   113  	dirPath, err := getPartialsBuildDir(buildName, buildNumber, projectKey)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	log.Debug("Creating temp build file at:", dirPath)
   118  	tempFile, err := os.CreateTemp(dirPath, "temp")
   119  	if err != nil {
   120  		return err
   121  	}
   122  	defer func() {
   123  		err = errors.Join(err, errorutils.CheckError(tempFile.Close()))
   124  	}()
   125  	_, err = tempFile.Write(content.Bytes())
   126  	return err
   127  }
   128  
   129  func SaveBuildInfo(buildName, buildNumber, projectKey string, buildInfo *buildInfo.BuildInfo) (err error) {
   130  	b, err := json.Marshal(buildInfo)
   131  	if errorutils.CheckError(err) != nil {
   132  		return err
   133  	}
   134  	var content bytes.Buffer
   135  	err = json.Indent(&content, b, "", "  ")
   136  	if errorutils.CheckError(err) != nil {
   137  		return err
   138  	}
   139  	dirPath, err := GetBuildDir(buildName, buildNumber, projectKey)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	log.Debug("Creating temp build file at: " + dirPath)
   144  	tempFile, err := os.CreateTemp(dirPath, "temp")
   145  	if errorutils.CheckError(err) != nil {
   146  		return err
   147  	}
   148  	defer func() {
   149  		err = errors.Join(err, errorutils.CheckError(tempFile.Close()))
   150  	}()
   151  	_, err = tempFile.Write(content.Bytes())
   152  	return errorutils.CheckError(err)
   153  }
   154  
   155  func SaveBuildGeneralDetails(buildName, buildNumber, projectKey string) error {
   156  	partialsBuildDir, err := getPartialsBuildDir(buildName, buildNumber, projectKey)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	log.Debug("Saving build general details at: " + partialsBuildDir)
   161  	detailsFilePath := filepath.Join(partialsBuildDir, BuildInfoDetails)
   162  	var exists bool
   163  	exists, err = fileutils.IsFileExists(detailsFilePath, false)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	if exists {
   168  		return nil
   169  	}
   170  	meta := buildInfo.General{
   171  		Timestamp: time.Now(),
   172  	}
   173  	b, err := json.Marshal(&meta)
   174  	if err != nil {
   175  		return errorutils.CheckError(err)
   176  	}
   177  	var content bytes.Buffer
   178  	err = json.Indent(&content, b, "", "  ")
   179  	if err != nil {
   180  		return errorutils.CheckError(err)
   181  	}
   182  	err = os.WriteFile(detailsFilePath, content.Bytes(), 0600)
   183  	return errorutils.CheckError(err)
   184  }
   185  
   186  type populatePartialBuildInfo func(partial *buildInfo.Partial)
   187  
   188  func SavePartialBuildInfo(buildName, buildNumber, projectKey string, populatePartialBuildInfoFunc populatePartialBuildInfo) error {
   189  	partialBuildInfo := new(buildInfo.Partial)
   190  	partialBuildInfo.Timestamp = time.Now().UnixNano() / int64(time.Millisecond)
   191  	populatePartialBuildInfoFunc(partialBuildInfo)
   192  	return saveBuildData(partialBuildInfo, buildName, buildNumber, projectKey)
   193  }
   194  
   195  func GetGeneratedBuildsInfo(buildName, buildNumber, projectKey string) ([]*buildInfo.BuildInfo, error) {
   196  	buildDir, err := GetBuildDir(buildName, buildNumber, projectKey)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	buildFiles, err := fileutils.ListFiles(buildDir, false)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	var generatedBuildsInfo []*buildInfo.BuildInfo
   206  	for _, buildFile := range buildFiles {
   207  		dir, err := fileutils.IsDirExists(buildFile, false)
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  		if dir {
   212  			continue
   213  		}
   214  		content, err := fileutils.ReadFile(buildFile)
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  		buildInfo := new(buildInfo.BuildInfo)
   219  		err = json.Unmarshal(content, &buildInfo)
   220  		if errorutils.CheckError(err) != nil {
   221  			return nil, err
   222  		}
   223  		generatedBuildsInfo = append(generatedBuildsInfo, buildInfo)
   224  	}
   225  	return generatedBuildsInfo, nil
   226  }
   227  
   228  func ReadPartialBuildInfoFiles(buildName, buildNumber, projectKey string) (buildInfo.Partials, error) {
   229  	var partials buildInfo.Partials
   230  	partialsBuildDir, err := getPartialsBuildDir(buildName, buildNumber, projectKey)
   231  	if err != nil {
   232  		return nil, err
   233  	}
   234  	buildFiles, err := fileutils.ListFiles(partialsBuildDir, false)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	for _, buildFile := range buildFiles {
   239  		dir, err := fileutils.IsDirExists(buildFile, false)
   240  		if err != nil {
   241  			return nil, err
   242  		}
   243  		if dir {
   244  			continue
   245  		}
   246  		if strings.HasSuffix(buildFile, BuildInfoDetails) {
   247  			continue
   248  		}
   249  		content, err := fileutils.ReadFile(buildFile)
   250  		if err != nil {
   251  			return nil, err
   252  		}
   253  		partial := new(buildInfo.Partial)
   254  		err = json.Unmarshal(content, &partial)
   255  		if errorutils.CheckError(err) != nil {
   256  			return nil, err
   257  		}
   258  		partials = append(partials, partial)
   259  	}
   260  
   261  	return partials, nil
   262  }
   263  
   264  func ReadBuildInfoGeneralDetails(buildName, buildNumber, projectKey string) (*buildInfo.General, error) {
   265  	partialsBuildDir, err := getPartialsBuildDir(buildName, buildNumber, projectKey)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	generalDetailsFilePath := filepath.Join(partialsBuildDir, BuildInfoDetails)
   270  	fileExists, err := fileutils.IsFileExists(generalDetailsFilePath, false)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	if !fileExists {
   275  		var buildString string
   276  		if projectKey != "" {
   277  			buildString = fmt.Sprintf("build-name: <%s>, build-number: <%s> and project: <%s>", buildName, buildNumber, projectKey)
   278  		} else {
   279  			buildString = fmt.Sprintf("build-name: <%s> and build-number: <%s>", buildName, buildNumber)
   280  		}
   281  		return nil, errors.New("Failed to construct the build-info to be published. " +
   282  			"This may be because there were no previous commands, which collected build-info for " + buildString)
   283  	}
   284  	content, err := fileutils.ReadFile(generalDetailsFilePath)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	details := new(buildInfo.General)
   289  	err = json.Unmarshal(content, &details)
   290  	if errorutils.CheckError(err) != nil {
   291  		return nil, err
   292  	}
   293  	return details, nil
   294  }
   295  
   296  func RemoveBuildDir(buildName, buildNumber, projectKey string) error {
   297  	tempDirPath, err := GetBuildDir(buildName, buildNumber, projectKey)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	exists, err := fileutils.IsDirExists(tempDirPath, false)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	if exists {
   306  		return errorutils.CheckError(fileutils.RemoveTempDir(tempDirPath))
   307  	}
   308  	return nil
   309  }
   310  
   311  type BuildConfiguration struct {
   312  	buildName            string
   313  	buildNumber          string
   314  	module               string
   315  	project              string
   316  	loadedFromConfigFile bool
   317  }
   318  
   319  func NewBuildConfiguration(buildName, buildNumber, module, project string) *BuildConfiguration {
   320  	return &BuildConfiguration{buildName: buildName, buildNumber: buildNumber, module: module, project: project}
   321  }
   322  
   323  func (bc *BuildConfiguration) SetBuildName(buildName string) *BuildConfiguration {
   324  	bc.buildName = buildName
   325  	return bc
   326  }
   327  
   328  func (bc *BuildConfiguration) SetBuildNumber(buildNumber string) *BuildConfiguration {
   329  	bc.buildNumber = buildNumber
   330  	return bc
   331  }
   332  
   333  func (bc *BuildConfiguration) SetProject(project string) *BuildConfiguration {
   334  	bc.project = project
   335  	return bc
   336  }
   337  
   338  func (bc *BuildConfiguration) SetModule(module string) *BuildConfiguration {
   339  	bc.module = module
   340  	return bc
   341  }
   342  
   343  func (bc *BuildConfiguration) GetBuildName() (string, error) {
   344  	if bc.buildName != "" {
   345  		return bc.buildName, nil
   346  	}
   347  	// Resolve from env var.
   348  	if bc.buildName = os.Getenv(coreutils.BuildName); bc.buildName != "" {
   349  		return bc.buildName, nil
   350  	}
   351  	// Resolve from config file in '.jfrog' folder.
   352  	var err error
   353  	if bc.buildName, err = bc.getBuildNameFromConfigFile(); bc.buildName != "" {
   354  		bc.loadedFromConfigFile = true
   355  	}
   356  	return bc.buildName, err
   357  }
   358  
   359  func (bc *BuildConfiguration) getBuildNameFromConfigFile() (string, error) {
   360  	confFilePath, exist, err := project.GetProjectConfFilePath(project.Build)
   361  	if os.IsPermission(err) {
   362  		log.Debug("The 'build-name' cannot be read from JFrog config due to permission denied.")
   363  		return "", nil
   364  	}
   365  	if err != nil || !exist {
   366  		return "", err
   367  	}
   368  	vConfig, err := project.ReadConfigFile(confFilePath, project.YAML)
   369  	if err != nil || vConfig == nil {
   370  		return "", err
   371  	}
   372  	return vConfig.GetString(ProjectConfigBuildNameKey), nil
   373  }
   374  
   375  func (bc *BuildConfiguration) GetBuildNumber() (string, error) {
   376  	if bc.buildNumber != "" {
   377  		return bc.buildNumber, nil
   378  	}
   379  	// Resolve from env var.
   380  	if bc.buildNumber = os.Getenv(coreutils.BuildNumber); bc.buildNumber != "" {
   381  		return bc.buildNumber, nil
   382  	}
   383  	// If build name was resolve from build.yaml file, use 'LATEST' as build number.
   384  	buildName, err := bc.GetBuildName()
   385  	if err != nil {
   386  		return "", err
   387  	}
   388  	if buildName != "" && bc.loadedFromConfigFile {
   389  		bc.buildNumber = artClientUtils.LatestBuildNumberKey
   390  	}
   391  	return bc.buildNumber, nil
   392  }
   393  
   394  func (bc *BuildConfiguration) GetProject() string {
   395  	if bc.project != "" {
   396  		return bc.project
   397  	}
   398  	// Resolve from env var.
   399  	bc.project = os.Getenv(coreutils.Project)
   400  	return bc.project
   401  }
   402  
   403  func (bc *BuildConfiguration) GetModule() string {
   404  	return bc.module
   405  }
   406  
   407  // Validates:
   408  // 1. If the build number exists, the build name also exists (and vice versa).
   409  // 2. If the modules exist, the build name/number are also exist (and vice versa).
   410  func (bc *BuildConfiguration) ValidateBuildAndModuleParams() error {
   411  	buildName, err := bc.GetBuildName()
   412  	if err != nil {
   413  		return err
   414  	}
   415  	buildNumber, err := bc.GetBuildNumber()
   416  	if err != nil {
   417  		return err
   418  	}
   419  	module := bc.GetModule()
   420  	if err := bc.ValidateBuildParams(); err != nil {
   421  		return err
   422  	}
   423  	if module != "" && buildName == "" && buildNumber == "" {
   424  		return errorutils.CheckErrorf("the build-name and build-number options are mandatory when the module option is provided.")
   425  	}
   426  	return nil
   427  }
   428  
   429  // Validates that if the build number exists, the build name also exists (and vice versa).
   430  func (bc *BuildConfiguration) ValidateBuildParams() error {
   431  	buildName, err := bc.GetBuildName()
   432  	if err != nil {
   433  		return err
   434  	}
   435  	buildNumber, err := bc.GetBuildNumber()
   436  	if err != nil {
   437  		return err
   438  	}
   439  	if (buildName == "" && buildNumber != "") || (buildName != "" && buildNumber == "") {
   440  		return errorutils.CheckErrorf("the build-name and build-number options cannot be provided separately")
   441  	}
   442  	return nil
   443  }
   444  
   445  func (bc *BuildConfiguration) IsCollectBuildInfo() (bool, error) {
   446  	if bc == nil {
   447  		return false, nil
   448  	}
   449  	buildName, err := bc.GetBuildName()
   450  	if err != nil {
   451  		return false, err
   452  	}
   453  	buildNumber, err := bc.GetBuildNumber()
   454  	if err != nil {
   455  		return false, err
   456  	}
   457  	return buildNumber != "" && buildName != "", nil
   458  }
   459  
   460  func (bc *BuildConfiguration) IsLoadedFromConfigFile() bool {
   461  	return bc.loadedFromConfigFile
   462  }
   463  
   464  func PopulateBuildArtifactsAsPartials(buildArtifacts []buildInfo.Artifact, buildConfiguration *BuildConfiguration, moduleType buildInfo.ModuleType) error {
   465  	populateFunc := func(partial *buildInfo.Partial) {
   466  		partial.Artifacts = buildArtifacts
   467  		partial.ModuleId = buildConfiguration.GetModule()
   468  		partial.ModuleType = moduleType
   469  	}
   470  	buildName, err := buildConfiguration.GetBuildName()
   471  	if err != nil {
   472  		return err
   473  	}
   474  	buildNumber, err := buildConfiguration.GetBuildNumber()
   475  	if err != nil {
   476  		return err
   477  	}
   478  	return SavePartialBuildInfo(buildName, buildNumber, buildConfiguration.GetProject(), populateFunc)
   479  }
   480  
   481  func CreateBuildPropsFromConfiguration(buildConfiguration *BuildConfiguration) (string, error) {
   482  	buildName, err := buildConfiguration.GetBuildName()
   483  	if err != nil {
   484  		return "", err
   485  	}
   486  	buildNumber, err := buildConfiguration.GetBuildNumber()
   487  	if err != nil {
   488  		return "", err
   489  	}
   490  	err = SaveBuildGeneralDetails(buildName, buildNumber, buildConfiguration.GetProject())
   491  	if err != nil {
   492  		return "", err
   493  	}
   494  	return CreateBuildProperties(buildName, buildNumber, buildConfiguration.GetProject())
   495  }