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  }