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 }