github.com/cobalt77/jfrog-client-go@v0.14.5/artifactory/services/utils/artifactoryutils.go (about) 1 package utils 2 3 import ( 4 "encoding/json" 5 "errors" 6 "net/http" 7 "net/url" 8 "strings" 9 "sync" 10 11 rthttpclient "github.com/cobalt77/jfrog-client-go/artifactory/httpclient" 12 "github.com/cobalt77/jfrog-client-go/auth" 13 "github.com/cobalt77/jfrog-client-go/httpclient" 14 "github.com/cobalt77/jfrog-client-go/utils" 15 "github.com/cobalt77/jfrog-client-go/utils/errorutils" 16 clientio "github.com/cobalt77/jfrog-client-go/utils/io" 17 "github.com/cobalt77/jfrog-client-go/utils/io/content" 18 "github.com/cobalt77/jfrog-client-go/utils/io/fileutils" 19 "github.com/cobalt77/jfrog-client-go/utils/io/httputils" 20 "github.com/cobalt77/jfrog-client-go/utils/log" 21 ) 22 23 const ( 24 ARTIFACTORY_SYMLINK = "symlink.dest" 25 SYMLINK_SHA1 = "symlink.destsha1" 26 Latest = "LATEST" 27 LastRelease = "LAST_RELEASE" 28 ) 29 30 func UploadFile(localPath, url, logMsgPrefix string, artifactoryDetails *auth.ServiceDetails, details *fileutils.FileDetails, 31 httpClientsDetails httputils.HttpClientDetails, client *rthttpclient.ArtifactoryHttpClient, retries int, progress clientio.Progress) (*http.Response, []byte, error) { 32 var err error 33 if details == nil { 34 details, err = fileutils.GetFileDetails(localPath) 35 } 36 if err != nil { 37 return nil, nil, err 38 } 39 40 requestClientDetails := httpClientsDetails.Clone() 41 AddChecksumHeaders(requestClientDetails.Headers, details) 42 AddAuthHeaders(requestClientDetails.Headers, *artifactoryDetails) 43 44 return client.UploadFile(localPath, url, logMsgPrefix, requestClientDetails, retries, progress) 45 } 46 47 func AddChecksumHeaders(headers map[string]string, fileDetails *fileutils.FileDetails) { 48 AddHeader("X-Checksum-Sha1", fileDetails.Checksum.Sha1, &headers) 49 AddHeader("X-Checksum-Md5", fileDetails.Checksum.Md5, &headers) 50 if len(fileDetails.Checksum.Sha256) > 0 { 51 AddHeader("X-Checksum", fileDetails.Checksum.Sha256, &headers) 52 } 53 } 54 55 func AddAuthHeaders(headers map[string]string, artifactoryDetails auth.ServiceDetails) { 56 if headers == nil { 57 headers = make(map[string]string) 58 } 59 if artifactoryDetails.GetSshAuthHeaders() != nil { 60 utils.MergeMaps(artifactoryDetails.GetSshAuthHeaders(), headers) 61 } 62 } 63 64 func SetContentType(contentType string, headers *map[string]string) { 65 AddHeader("Content-Type", contentType, headers) 66 } 67 68 func DisableAccelBuffering(headers *map[string]string) { 69 AddHeader("X-Accel-Buffering", "no", headers) 70 } 71 72 func AddHeader(headerName, headerValue string, headers *map[string]string) { 73 if *headers == nil { 74 *headers = make(map[string]string) 75 } 76 (*headers)[headerName] = headerValue 77 } 78 79 func BuildArtifactoryUrl(baseUrl, path string, params map[string]string) (string, error) { 80 u := url.URL{Path: path} 81 escapedUrl, err := url.Parse(baseUrl + u.String()) 82 err = errorutils.CheckError(err) 83 if err != nil { 84 return "", err 85 } 86 q := escapedUrl.Query() 87 for k, v := range params { 88 q.Set(k, v) 89 } 90 escapedUrl.RawQuery = q.Encode() 91 return escapedUrl.String(), nil 92 } 93 94 func IsWildcardPattern(pattern string) bool { 95 return strings.Contains(pattern, "*") || strings.HasSuffix(pattern, "/") || !strings.Contains(pattern, "/") 96 } 97 98 // paths - Sorted array. 99 // index - Index of the current path which we want to check if it a prefix of any of the other previous paths. 100 // separator - File separator. 101 // Returns true paths[index] is a prefix of any of the paths[i] where i<index, otherwise returns false. 102 func IsSubPath(paths []string, index int, separator string) bool { 103 currentPath := paths[index] 104 if !strings.HasSuffix(currentPath, separator) { 105 currentPath += separator 106 } 107 for i := index - 1; i >= 0; i-- { 108 if strings.HasPrefix(paths[i], currentPath) { 109 return true 110 } 111 } 112 return false 113 } 114 115 // This method parses buildIdentifier. buildIdentifier should be from the format "buildName/buildNumber". 116 // If no buildNumber provided LATEST will be downloaded. 117 // If buildName or buildNumber contains "/" (slash) it should be escaped by "\" (backslash). 118 // Result examples of parsing: "aaa/123" > "aaa"-"123", "aaa" > "aaa"-"LATEST", "aaa\\/aaa" > "aaa/aaa"-"LATEST", "aaa/12\\/3" > "aaa"-"12/3". 119 func getBuildNameAndNumberFromBuildIdentifier(buildIdentifier string, flags CommonConf) (string, string, error) { 120 buildName, buildNumber, err := parseNameAndVersion(buildIdentifier, true) 121 if err != nil { 122 return "", "", err 123 } 124 return GetBuildNameAndNumberFromArtifactory(buildName, buildNumber, flags) 125 } 126 127 func GetBuildNameAndNumberFromArtifactory(buildName, buildNumber string, flags CommonConf) (string, string, error) { 128 if buildNumber == Latest || buildNumber == LastRelease { 129 return getLatestBuildNumberFromArtifactory(buildName, buildNumber, flags) 130 } 131 return buildName, buildNumber, nil 132 } 133 134 func getBuildNameAndNumberFromProps(properties []Property) (buildName string, buildNumber string) { 135 for _, property := range properties { 136 if property.Key == "build.name" { 137 buildName = property.Value 138 } else if property.Key == "build.number" { 139 buildNumber = property.Value 140 } 141 if len(buildName) > 0 && len(buildNumber) > 0 { 142 return buildName, buildNumber 143 } 144 } 145 return 146 } 147 148 // For builds (useLatestPolicy = true) - Parse build name and number. The build number can be LATEST if absent. 149 // For release bundles - Parse bundle name and version. 150 func parseNameAndVersion(identifier string, useLatestPolicy bool) (string, string, error) { 151 const Delimiter = "/" 152 const EscapeChar = "\\" 153 154 if identifier == "" { 155 return "", "", nil 156 } 157 if !strings.Contains(identifier, Delimiter) { 158 if useLatestPolicy { 159 log.Debug("No '" + Delimiter + "' is found in the build, build number is set to " + Latest) 160 return identifier, Latest, nil 161 } else { 162 return "", "", errorutils.CheckError(errors.New("No '" + Delimiter + "' is found in the bundle")) 163 } 164 } 165 name, version := "", "" 166 versionsArray := []string{} 167 identifiers := strings.Split(identifier, Delimiter) 168 // The delimiter must not be prefixed with escapeChar (if it is, it should be part of the version) 169 // the code below gets substring from before the last delimiter. 170 // If the new string ends with escape char it means the last delimiter was part of the version and we need 171 // to go back to the previous delimiter. 172 // If no proper delimiter was found the full string will be the name. 173 for i := len(identifiers) - 1; i >= 1; i-- { 174 versionsArray = append([]string{identifiers[i]}, versionsArray...) 175 if !strings.HasSuffix(identifiers[i-1], EscapeChar) { 176 name = strings.Join(identifiers[:i], Delimiter) 177 version = strings.Join(versionsArray, Delimiter) 178 break 179 } 180 } 181 if name == "" { 182 if useLatestPolicy { 183 log.Debug("No delimiter char (" + Delimiter + ") without escaping char was found in the build, build number is set to " + Latest) 184 name = identifier 185 version = Latest 186 } else { 187 return "", "", errorutils.CheckError(errors.New("No delimiter char (" + Delimiter + ") without escaping char was found in the bundle")) 188 } 189 } 190 // Remove escape chars. 191 name = strings.Replace(name, "\\/", "/", -1) 192 version = strings.Replace(version, "\\/", "/", -1) 193 return name, version, nil 194 } 195 196 type build struct { 197 BuildName string `json:"buildName"` 198 BuildNumber string `json:"buildNumber"` 199 } 200 201 func getLatestBuildNumberFromArtifactory(buildName, buildNumber string, flags CommonConf) (string, string, error) { 202 restUrl := flags.GetArtifactoryDetails().GetUrl() + "api/build/patternArtifacts" 203 body, err := createBodyForLatestBuildRequest(buildName, buildNumber) 204 if err != nil { 205 return "", "", err 206 } 207 log.Debug("Getting build name and number from Artifactory: " + buildName + ", " + buildNumber) 208 httpClientsDetails := flags.GetArtifactoryDetails().CreateHttpClientDetails() 209 SetContentType("application/json", &httpClientsDetails.Headers) 210 log.Debug("Sending post request to: " + restUrl + ", with the following body: " + string(body)) 211 client, err := httpclient.ClientBuilder().Build() 212 if err != nil { 213 return "", "", err 214 } 215 resp, body, err := client.SendPost(restUrl, body, httpClientsDetails) 216 if err != nil { 217 return "", "", err 218 } 219 if resp.StatusCode != http.StatusOK { 220 return "", "", errorutils.CheckError(errors.New("Artifactory response: " + resp.Status + "\n" + utils.IndentJson(body))) 221 } 222 log.Debug("Artifactory response: ", resp.Status) 223 var responseBuild []build 224 err = json.Unmarshal(body, &responseBuild) 225 if errorutils.CheckError(err) != nil { 226 return "", "", err 227 } 228 if responseBuild[0].BuildNumber != "" { 229 log.Debug("Found build number: " + responseBuild[0].BuildNumber) 230 } else { 231 log.Debug("The build could not be found in Artifactory") 232 } 233 234 return buildName, responseBuild[0].BuildNumber, nil 235 } 236 237 func createBodyForLatestBuildRequest(buildName, buildNumber string) (body []byte, err error) { 238 buildJsonArray := []build{{buildName, buildNumber}} 239 body, err = json.Marshal(buildJsonArray) 240 err = errorutils.CheckError(err) 241 return 242 } 243 244 func filterAqlSearchResultsByBuild(specFile *ArtifactoryCommonParams, reader *content.ContentReader, flags CommonConf, itemsAlreadyContainProperties bool) (*content.ContentReader, error) { 245 var aqlSearchErr error 246 var readerWithProps *content.ContentReader 247 var buildArtifactsSha1 map[string]int 248 var wg sync.WaitGroup 249 // If 'build-number' is missing in spec file, we fetch the laster from artifactory. 250 buildName, buildNumber, err := getBuildNameAndNumberFromBuildIdentifier(specFile.Build, flags) 251 if err != nil { 252 return nil, err 253 } 254 255 wg.Add(1) 256 // Get Sha1 for artifacts by build name and number 257 go func() { 258 buildArtifactsSha1, aqlSearchErr = fetchBuildArtifactsSha1(buildName, buildNumber, flags) 259 wg.Done() 260 }() 261 262 if !itemsAlreadyContainProperties { 263 // Add properties to the previously found artifacts (in case properties haven't already fetched from Artifactory) 264 readerWithProps, err = searchProps(specFile.Aql.ItemsFind, "build.name", buildName, flags) 265 if err != nil { 266 return nil, err 267 } 268 defer readerWithProps.Close() 269 tempReader, err := loadMissingProperties(reader, readerWithProps) 270 if err != nil { 271 return nil, err 272 } 273 defer tempReader.Close() 274 wg.Wait() 275 if aqlSearchErr != nil { 276 return nil, aqlSearchErr 277 } 278 return filterBuildAqlSearchResults(tempReader, buildArtifactsSha1, buildName, buildNumber) 279 } 280 281 wg.Wait() 282 if aqlSearchErr != nil { 283 return nil, aqlSearchErr 284 } 285 return filterBuildAqlSearchResults(reader, buildArtifactsSha1, buildName, buildNumber) 286 } 287 288 // Load all properties to the sorted result items. Save the new result items to a file. 289 // cr - Sorted result without properties 290 // crWithProps - Result item with properties 291 // Return a content reader which points to the result file. 292 func loadMissingProperties(reader *content.ContentReader, readerWithProps *content.ContentReader) (*content.ContentReader, error) { 293 // Key -> Relative path, value -> *ResultItem 294 // Contains a limited amount of items from a file, to not overflow memory. 295 buffer := make(map[string]*ResultItem) 296 var writeOrder []*ResultItem 297 var err error 298 // Create new file to write result output 299 resultFile, err := content.NewContentWriter(content.DefaultKey, true, false) 300 if err != nil { 301 return nil, err 302 } 303 defer resultFile.Close() 304 for resultItem := new(ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 305 buffer[resultItem.GetItemRelativePath()] = resultItem 306 // Since maps are an unordered collection, we use slice to save the order of the items 307 writeOrder = append(writeOrder, resultItem) 308 if len(buffer) == utils.MaxBufferSize { 309 // Buffer was full, write all data to a file. 310 err = updateProps(readerWithProps, resultFile, buffer, writeOrder) 311 if err != nil { 312 return nil, err 313 } 314 buffer = make(map[string]*ResultItem) 315 writeOrder = make([]*ResultItem, 0) 316 } 317 } 318 if reader.GetError() != nil { 319 return nil, err 320 } 321 reader.Reset() 322 if err := updateProps(readerWithProps, resultFile, buffer, writeOrder); err != nil { 323 return nil, err 324 } 325 return content.NewContentReader(resultFile.GetFilePath(), content.DefaultKey), nil 326 } 327 328 // Load the properties from readerWithProps into buffer's ResultItem and write its values into the resultWriter. 329 // buffer - Search result buffer Key -> relative path, value -> ResultItem. We use this to load the props into the item by matching the uniqueness of relevant path. 330 // crWithProps - File containing all the results with proprties. 331 // writeOrder - List of sorted buffer's searchResults(Map is an unordered collection). 332 // resultWriter - Search results (sorted) with props. 333 func updateProps(readerWithProps *content.ContentReader, resultWriter *content.ContentWriter, buffer map[string]*ResultItem, writeOrder []*ResultItem) error { 334 if len(buffer) == 0 { 335 return nil 336 } 337 // Load buffer items with their properties. 338 for resultItem := new(ResultItem); readerWithProps.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 339 if value, ok := buffer[resultItem.GetItemRelativePath()]; ok { 340 value.Properties = resultItem.Properties 341 } 342 } 343 if err := readerWithProps.GetError(); err != nil { 344 return err 345 } 346 readerWithProps.Reset() 347 // Write the items to a file with the same search result order. 348 for _, itemToWrite := range writeOrder { 349 resultWriter.Write(*itemToWrite) 350 } 351 return nil 352 } 353 354 // Run AQL to retrieve all artifacts associated with a specific build. 355 // Return a map of the artifacts SHA1. 356 func fetchBuildArtifactsSha1(buildName, buildNumber string, flags CommonConf) (map[string]int, error) { 357 buildQuery := createAqlQueryForBuild(buildName, buildNumber, buildIncludeQueryPart([]string{"name", "repo", "path", "actual_sha1"})) 358 reader, err := aqlSearch(buildQuery, flags) 359 if err != nil { 360 return nil, err 361 } 362 defer reader.Close() 363 return extractSha1FromAqlResponse(reader) 364 } 365 366 // Find artifacts with a specific property. 367 // aqlBody - AQL to execute together with property filter. 368 // filterByPropName - Property name to filter. 369 // filterByPropValue - Property value to filter. 370 // flags - Command flags for AQL execution. 371 func searchProps(aqlBody, filterByPropName, filterByPropValue string, flags CommonConf) (*content.ContentReader, error) { 372 return ExecAqlSaveToFile(createPropsQuery(aqlBody, filterByPropName, filterByPropValue), flags) 373 } 374 375 // Gets a reader of AQL results, and return map with all the SHA1's as keys. 376 // The values for all the keys in the map is 2 377 func extractSha1FromAqlResponse(reader *content.ContentReader) (elementsMap map[string]int, err error) { 378 elementsMap = make(map[string]int) 379 for resultItem := new(ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 380 elementsMap[resultItem.Actual_Sha1] = 2 381 } 382 if err = reader.GetError(); err != nil { 383 return 384 } 385 reader.Reset() 386 return 387 } 388 389 // Returns a filtered search result file. 390 // Map each search result in one of three priority files: 391 // 1st priority: Match {Sha1, build name, build number} 392 // 2nd priority: Match {Sha1, build name} 393 // 3rd priority: Match {Sha1} 394 // As a result, any duplicated search result item will be split into a different priority list. 395 // Then merge all the priority list into a single file, so each item is present once in the result file according to the priority list. 396 // Side note: For each priority level, a single SHA1 can match multi artifacts under different modules. 397 // reader - Reader of the aql result. 398 // buildArtifactsSha - Map of all the build-name's sha1 as keys and int as its values. The int value represents priority wheres 0 is a high priority and 2 is lowest. 399 func filterBuildAqlSearchResults(reader *content.ContentReader, buildArtifactsSha map[string]int, buildName, buildNumber string) (*content.ContentReader, error) { 400 priorityArray, err := createPrioritiesFiles() 401 if err != nil { 402 return nil, err 403 } 404 resultCw, err := content.NewContentWriter(content.DefaultKey, true, false) 405 if err != nil { 406 return nil, err 407 } 408 defer resultCw.Close() 409 // Step 1 - Fill the priority files with search results. 410 for resultItem := new(ResultItem); reader.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 411 if _, ok := buildArtifactsSha[resultItem.Actual_Sha1]; !ok { 412 continue 413 } 414 resultBuildName, resultBuildNumber := getBuildNameAndNumberFromProps(resultItem.Properties) 415 isBuildNameMatched := resultBuildName == buildName 416 if isBuildNameMatched && resultBuildNumber == buildNumber { 417 priorityArray[0].Write(*resultItem) 418 buildArtifactsSha[resultItem.Actual_Sha1] = 0 419 continue 420 } 421 if isBuildNameMatched && buildArtifactsSha[resultItem.Actual_Sha1] != 0 { 422 priorityArray[1].Write(*resultItem) 423 buildArtifactsSha[resultItem.Actual_Sha1] = 1 424 continue 425 } 426 if buildArtifactsSha[resultItem.Actual_Sha1] == 2 { 427 priorityArray[2].Write(*resultItem) 428 } 429 } 430 if err = reader.GetError(); err != nil { 431 return nil, err 432 } 433 reader.Reset() 434 var priorityLevel int = 0 435 // Step 2 - Append the files to the final results file. 436 // Scan each priority artifacts and apply them to the final result, skip results that have been already written, by higher priority. 437 for _, priority := range priorityArray { 438 if err = priority.Close(); err != nil { 439 return nil, err 440 } 441 temp := content.NewContentReader(priority.GetFilePath(), content.DefaultKey) 442 for resultItem := new(ResultItem); temp.NextRecord(resultItem) == nil; resultItem = new(ResultItem) { 443 if buildArtifactsSha[resultItem.Actual_Sha1] == priorityLevel { 444 resultCw.Write(*resultItem) 445 } 446 } 447 if err = temp.GetError(); err != nil { 448 return nil, err 449 } 450 if err = temp.Close(); err != nil { 451 return nil, err 452 } 453 priorityLevel++ 454 } 455 return content.NewContentReader(resultCw.GetFilePath(), content.DefaultKey), nil 456 } 457 458 // Create priority files. 459 func createPrioritiesFiles() ([]*content.ContentWriter, error) { 460 firstPriority, err := content.NewContentWriter(content.DefaultKey, true, false) 461 if err != nil { 462 return nil, err 463 } 464 secondPriority, err := content.NewContentWriter(content.DefaultKey, true, false) 465 if err != nil { 466 return nil, err 467 } 468 thirdPriority, err := content.NewContentWriter(content.DefaultKey, true, false) 469 if err != nil { 470 return nil, err 471 } 472 return []*content.ContentWriter{firstPriority, secondPriority, thirdPriority}, nil 473 } 474 475 type CommonConf interface { 476 GetArtifactoryDetails() auth.ServiceDetails 477 SetArtifactoryDetails(rt auth.ServiceDetails) 478 GetJfrogHttpClient() (*rthttpclient.ArtifactoryHttpClient, error) 479 IsDryRun() bool 480 } 481 482 type CommonConfImpl struct { 483 artDetails auth.ServiceDetails 484 DryRun bool 485 } 486 487 func (flags *CommonConfImpl) GetArtifactoryDetails() auth.ServiceDetails { 488 return flags.artDetails 489 } 490 491 func (flags *CommonConfImpl) SetArtifactoryDetails(rt auth.ServiceDetails) { 492 flags.artDetails = rt 493 } 494 495 func (flags *CommonConfImpl) IsDryRun() bool { 496 return flags.DryRun 497 } 498 499 func (flags *CommonConfImpl) GetJfrogHttpClient() (*rthttpclient.ArtifactoryHttpClient, error) { 500 return rthttpclient.ArtifactoryClientBuilder().SetServiceDetails(&flags.artDetails).Build() 501 }