github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/transferfiles/longpropertycheck.go (about)

     1  package transferfiles
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"sync"
     9  
    10  	"github.com/jfrog/gofrog/parallel"
    11  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/transferfiles/api"
    12  	cmdutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
    13  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils/precheckrunner"
    14  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    15  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    16  	"github.com/jfrog/jfrog-cli-core/v2/utils/progressbar"
    17  	"github.com/jfrog/jfrog-client-go/artifactory"
    18  	servicesUtils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
    19  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    20  	"golang.org/x/exp/slices"
    21  
    22  	"github.com/jfrog/jfrog-client-go/utils/log"
    23  
    24  	"time"
    25  
    26  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    27  )
    28  
    29  const (
    30  	propertyAqlPaginationLimit = 1000000
    31  	maxThreadCapacity          = 5000000
    32  	threadCount                = 10
    33  	maxAllowedValLength        = 2400
    34  	longPropertyCheckName      = "Properties with value longer than 2.4K characters"
    35  	propertiesRequestTimeout   = time.Minute * 30
    36  )
    37  
    38  // Property - Represents a property of an item
    39  type Property struct {
    40  	Key   string `json:"key,omitempty"`
    41  	Value string `json:"value,omitempty"`
    42  }
    43  
    44  // valueLength - Equals to the value length
    45  func (p *Property) valueLength() uint {
    46  	return uint(len(p.Value))
    47  }
    48  
    49  // FileWithLongProperty - Represent a failed instance; File with property that has failed the check (used for csv report format)
    50  type FileWithLongProperty struct {
    51  	// The file that contains the long property
    52  	api.FileRepresentation
    53  	// The length of the property's value
    54  	Length uint `json:"value-length,omitempty"`
    55  	// The property that failed the check
    56  	Property
    57  }
    58  
    59  type LongPropertyCheck struct {
    60  	producerConsumer       parallel.Runner
    61  	filesChan              chan FileWithLongProperty
    62  	errorsQueue            *clientutils.ErrorsQueue
    63  	repos                  []string
    64  	disabledDistinctiveAql bool
    65  }
    66  
    67  func NewLongPropertyCheck(repos []string, disabledDistinctiveAql bool) *LongPropertyCheck {
    68  	return &LongPropertyCheck{repos: repos, disabledDistinctiveAql: disabledDistinctiveAql}
    69  }
    70  
    71  func (lpc *LongPropertyCheck) Name() string {
    72  	return longPropertyCheckName
    73  }
    74  
    75  func (lpc *LongPropertyCheck) ExecuteCheck(args precheckrunner.RunArguments) (passed bool, err error) {
    76  	// Init producer consumer
    77  	lpc.producerConsumer = parallel.NewRunner(threadCount, maxThreadCapacity, false)
    78  	lpc.filesChan = make(chan FileWithLongProperty, threadCount)
    79  	lpc.errorsQueue = clientutils.NewErrorsQueue(1)
    80  	var waitCollection sync.WaitGroup
    81  	var filesWithLongProperty []FileWithLongProperty
    82  	// Handle progress display
    83  	var progress *progressbar.TasksProgressBar
    84  	if args.ProgressMng != nil {
    85  		progress = args.ProgressMng.NewTasksProgressBar(0, coreutils.IsWindows(), "long property")
    86  		defer progress.GetBar().Abort(true)
    87  	}
    88  	// Create consumer routine to collect the files from the search tasks
    89  	waitCollection.Add(1)
    90  	go func() {
    91  		for current := range lpc.filesChan {
    92  			filesWithLongProperty = append(filesWithLongProperty, current)
    93  		}
    94  		waitCollection.Done()
    95  	}()
    96  	// Create producer routine to create search tasks for long properties in the server
    97  	go func() {
    98  		defer lpc.producerConsumer.Done()
    99  		lpc.longPropertiesTaskProducer(progress, args)
   100  	}()
   101  	// Run
   102  	lpc.producerConsumer.Run()
   103  	close(lpc.filesChan)
   104  	waitCollection.Wait()
   105  	if err = lpc.errorsQueue.GetError(); err != nil {
   106  		return
   107  	}
   108  	// Result
   109  	if len(filesWithLongProperty) != 0 {
   110  		err = handleFailureRun(filesWithLongProperty)
   111  	} else {
   112  		passed = true
   113  	}
   114  	return
   115  }
   116  
   117  // Search for long properties in the server and create a search task to find the files that contains them
   118  // Returns the number of long properties found
   119  func (lpc *LongPropertyCheck) longPropertiesTaskProducer(progress *progressbar.TasksProgressBar, args precheckrunner.RunArguments) int {
   120  	// Init
   121  	serviceManager, err := utils.CreateServiceManagerWithContext(args.Context, args.ServerDetails, false, 0, retries, retriesWaitMilliSecs, propertiesRequestTimeout)
   122  	if err != nil {
   123  		return 0
   124  	}
   125  	var propertyQuery *AqlPropertySearchResult
   126  	longPropertiesCount := 0
   127  	pageCounter := 0
   128  	// Search
   129  	for {
   130  		if propertyQuery, err = runSearchPropertyAql(serviceManager, pageCounter); err != nil {
   131  			return 0
   132  		}
   133  		log.Debug(fmt.Sprintf("Found %d properties in the batch (isLastBatch=%t)", len(propertyQuery.Results), len(propertyQuery.Results) < propertyAqlPaginationLimit))
   134  		for _, property := range propertyQuery.Results {
   135  			if long := isLongProperty(property); long {
   136  				log.Debug(fmt.Sprintf(`Found long property ('@%s':'%s')`, property.Key, property.Value))
   137  				if lpc.producerConsumer != nil {
   138  					_, _ = lpc.producerConsumer.AddTaskWithError(lpc.createSearchPropertyTask(property, args, progress), lpc.errorsQueue.AddError)
   139  				}
   140  				if progress != nil {
   141  					progress.IncGeneralProgressTotalBy(1)
   142  				}
   143  				longPropertiesCount++
   144  			}
   145  		}
   146  		if len(propertyQuery.Results) < propertyAqlPaginationLimit {
   147  			break
   148  		}
   149  		pageCounter++
   150  	}
   151  
   152  	return longPropertiesCount
   153  }
   154  
   155  // Checks if the value of a property is not bigger than maxAllowedValLength
   156  func isLongProperty(property Property) bool {
   157  	return property.valueLength() > maxAllowedValLength
   158  }
   159  
   160  // AqlPropertySearchResult - the structure that returns from a property aql search
   161  type AqlPropertySearchResult struct {
   162  	Results []Property
   163  }
   164  
   165  // Get all the properties on the server using AQL (with pagination)
   166  func runSearchPropertyAql(serviceManager artifactory.ArtifactoryServicesManager, pageNumber int) (result *AqlPropertySearchResult, err error) {
   167  	result = &AqlPropertySearchResult{}
   168  	err = runAqlService(serviceManager, getSearchAllPropertiesQuery(pageNumber), result)
   169  	return
   170  }
   171  
   172  // Get the query that search properties on a server with pagination
   173  func getSearchAllPropertiesQuery(pageNumber int) string {
   174  	query := `properties.find()`
   175  	query += fmt.Sprintf(`.sort({"$asc":["key"]}).offset(%d).limit(%d)`, pageNumber*propertyAqlPaginationLimit, propertyAqlPaginationLimit)
   176  	return query
   177  }
   178  
   179  // Create a task that fetch from the server the files with the given property.
   180  // We keep only the files that are at the requested repos and pass them at the files channel
   181  func (lpc *LongPropertyCheck) createSearchPropertyTask(property Property, args precheckrunner.RunArguments, progress *progressbar.TasksProgressBar) parallel.TaskFunc {
   182  	return func(threadId int) (err error) {
   183  		serviceManager, err := utils.CreateServiceManagerWithContext(args.Context, args.ServerDetails, false, 0, retries, retriesWaitMilliSecs, propertiesRequestTimeout)
   184  		if err != nil {
   185  			return
   186  		}
   187  		// Search
   188  		var query *servicesUtils.AqlSearchResult
   189  		if query, err = lpc.runSearchPropertyInFilesAql(serviceManager, property); err != nil {
   190  			return
   191  		}
   192  		log.Debug(fmt.Sprintf("[Thread=%d] Got %d files from the query", threadId, len(query.Results)))
   193  		for _, item := range query.Results {
   194  			file := api.FileRepresentation{Repo: item.Repo, Path: item.Path, Name: item.Name}
   195  			// Keep only if in the requested repos
   196  			if slices.Contains(lpc.repos, file.Repo) {
   197  				fileWithLongProperty := FileWithLongProperty{file, property.valueLength(), property}
   198  				log.Debug(fmt.Sprintf("[Thread=%d] Found File{Repo=%s, Path=%s, Name=%s} with matching entry of long property.", threadId, file.Repo, file.Path, file.Name))
   199  				lpc.filesChan <- fileWithLongProperty
   200  			}
   201  		}
   202  		// Notify end of search for the current property
   203  		if args.ProgressMng != nil && progress != nil {
   204  			progress.GetBar().Increment()
   205  		}
   206  		return
   207  	}
   208  }
   209  
   210  // Get all the files that contains the given property using AQL
   211  func (lpc *LongPropertyCheck) runSearchPropertyInFilesAql(serviceManager artifactory.ArtifactoryServicesManager, property Property) (result *servicesUtils.AqlSearchResult, err error) {
   212  	result = &servicesUtils.AqlSearchResult{}
   213  	err = runAqlService(serviceManager, lpc.getSearchPropertyInFilesQuery(property), result)
   214  	return
   215  }
   216  
   217  // Get the query that search files with specific property
   218  func (lpc *LongPropertyCheck) getSearchPropertyInFilesQuery(property Property) string {
   219  	query := fmt.Sprintf(`items.find({"type": {"$eq":"any"},"@%s":"%s"}).include("repo","path","name")`, property.Key, property.Value)
   220  	query += appendDistinctIfNeeded(lpc.disabledDistinctiveAql)
   221  	return query
   222  }
   223  
   224  // Run AQL service that return a result in the given format structure 'v'
   225  func runAqlService(serviceManager artifactory.ArtifactoryServicesManager, query string, v any) (err error) {
   226  	reader, err := serviceManager.Aql(query)
   227  	if err != nil {
   228  		return
   229  	}
   230  	defer func() {
   231  		if reader != nil {
   232  			err = errors.Join(err, errorutils.CheckError(reader.Close()))
   233  		}
   234  	}()
   235  	respBody, err := io.ReadAll(reader)
   236  	if err != nil {
   237  		return errorutils.CheckError(err)
   238  	}
   239  	err = errorutils.CheckError(json.Unmarshal(respBody, v))
   240  	return
   241  }
   242  
   243  // Create csv summary of all the files with long properties and log the result
   244  func handleFailureRun(filesWithLongProperty []FileWithLongProperty) (err error) {
   245  	// Create summary
   246  	csvPath, err := cmdutils.CreateCSVFile("long-properties", filesWithLongProperty, time.Now())
   247  	if err != nil {
   248  		log.Error("Couldn't create the long properties CSV file", err)
   249  		return
   250  	}
   251  	// Log result
   252  	nFails := len(filesWithLongProperty)
   253  	propertyTxt := "entries"
   254  	if nFails == 1 {
   255  		propertyTxt = "entry"
   256  	}
   257  	log.Info(fmt.Sprintf("Found %d property %s with value longer than 2.4k characters. Check the summary CSV file in: %s", nFails, propertyTxt, csvPath))
   258  	return
   259  }