github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/utils/precheckrunner/remoteurlchecker.go (about)

     1  package precheckrunner
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/utils"
    10  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    11  	"github.com/jfrog/jfrog-cli-core/v2/utils/progressbar"
    12  	"github.com/jfrog/jfrog-client-go/artifactory"
    13  	"github.com/jfrog/jfrog-client-go/artifactory/services"
    14  	clientutils "github.com/jfrog/jfrog-client-go/utils"
    15  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    16  	"github.com/jfrog/jfrog-client-go/utils/io/httputils"
    17  	"github.com/jfrog/jfrog-client-go/utils/log"
    18  )
    19  
    20  type RemoteUrlCheckStatus string
    21  
    22  const (
    23  	remoteUrlCheckName              = "Remote repositories URL connectivity"
    24  	remoteUrlCheckPollingTimeout    = 30 * time.Minute
    25  	remoteUrlCheckPollingInterval   = 5 * time.Second
    26  	remoteUrlCheckRetries           = 3
    27  	remoteUrlCheckIntervalMilliSecs = 10000
    28  )
    29  
    30  type remoteRepoSettings struct {
    31  	Key         string `json:"key,omitempty"`
    32  	Url         string `json:"url,omitempty"`
    33  	RepoType    string `json:"repo_type,omitempty"`
    34  	Username    string `json:"username,omitempty"`
    35  	Password    string `json:"password,omitempty"`
    36  	QueryParams string `json:"query_params,omitempty"`
    37  }
    38  
    39  type remoteUrlResponse struct {
    40  	Status                   RemoteUrlCheckStatus     `json:"status,omitempty"`
    41  	InaccessibleRepositories []inaccessibleRepository `json:"inaccessible_repositories,omitempty"`
    42  	CheckedRepositories      uint                     `json:"checked_repositories,omitempty"`
    43  	TotalRepositories        uint                     `json:"total_repositories,omitempty"`
    44  }
    45  
    46  type inaccessibleRepository struct {
    47  	RepoKey    string `json:"repo_key,omitempty"`
    48  	StatusCode int    `json:"status_code,omitempty"`
    49  	Reason     string `json:"reason,omitempty"`
    50  	Url        string `json:"url,omitempty"`
    51  }
    52  
    53  // Run remote repository URLs accessibility test before transferring configuration from one Artifactory to another
    54  type RemoteRepositoryCheck struct {
    55  	targetServicesManager *artifactory.ArtifactoryServicesManager
    56  	remoteRepositories    []interface{}
    57  }
    58  
    59  func NewRemoteRepositoryCheck(targetServicesManager *artifactory.ArtifactoryServicesManager, remoteRepositories []interface{}) *RemoteRepositoryCheck {
    60  	return &RemoteRepositoryCheck{targetServicesManager, remoteRepositories}
    61  }
    62  
    63  func (rrc *RemoteRepositoryCheck) Name() string {
    64  	return remoteUrlCheckName
    65  }
    66  
    67  func (rrc *RemoteRepositoryCheck) ExecuteCheck(args RunArguments) (passed bool, err error) {
    68  	remoteUrlRequest, err := rrc.createRemoteUrlRequest()
    69  	if err != nil {
    70  		return false, err
    71  	}
    72  	inaccessibleRepositories, err := rrc.doCheckRemoteRepositories(args, remoteUrlRequest)
    73  	if err != nil {
    74  		return false, err
    75  	}
    76  	if len(*inaccessibleRepositories) == 0 {
    77  		return true, nil
    78  	}
    79  	return false, handleFailureRun(*inaccessibleRepositories)
    80  }
    81  
    82  // Create the remote URL request from the received remote repository details from Artifactory
    83  func (rrc *RemoteRepositoryCheck) createRemoteUrlRequest() ([]remoteRepoSettings, error) {
    84  	remoteUrlRequests := make([]remoteRepoSettings, len(rrc.remoteRepositories))
    85  	for i, remoteRepository := range rrc.remoteRepositories {
    86  		// The remote repository interface is not necessarily of RemoteRepositoryBaseParams
    87  		// type (can be a map) and therefore we marshal and unmarshal it.
    88  		remoteRepositoryBytes, err := json.Marshal(remoteRepository)
    89  		if err != nil {
    90  			return nil, errorutils.CheckError(err)
    91  		}
    92  		var remoteRepositoryParams services.RemoteRepositoryBaseParams
    93  		if err = json.Unmarshal(remoteRepositoryBytes, &remoteRepositoryParams); err != nil {
    94  			return nil, errorutils.CheckError(err)
    95  		}
    96  
    97  		remoteUrlRequests[i] = remoteRepoSettings{
    98  			Key:         remoteRepositoryParams.Key,
    99  			Url:         remoteRepositoryParams.Url,
   100  			RepoType:    remoteRepositoryParams.PackageType,
   101  			Username:    remoteRepositoryParams.Username,
   102  			Password:    remoteRepositoryParams.Password,
   103  			QueryParams: remoteRepositoryParams.QueryParams,
   104  		}
   105  	}
   106  	return remoteUrlRequests, nil
   107  }
   108  
   109  func (rrc *RemoteRepositoryCheck) doCheckRemoteRepositories(args RunArguments, remoteUrlRequest []remoteRepoSettings) (inaccessibleRepositories *[]inaccessibleRepository, err error) {
   110  	artifactoryUrl := clientutils.AddTrailingSlashIfNeeded(args.ServerDetails.ArtifactoryUrl)
   111  
   112  	body, err := json.Marshal(remoteUrlRequest)
   113  	if err != nil {
   114  		return nil, errorutils.CheckError(err)
   115  	}
   116  
   117  	// Create rtDetails
   118  	rtDetails, err := utils.CreateArtifactoryClientDetails(*rrc.targetServicesManager)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	progressBar, err := rrc.startCheckRemoteRepositories(rtDetails, artifactoryUrl, args, body)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	defer func() {
   128  		if progressBar != nil {
   129  			progressBar.GetBar().Abort(true)
   130  		}
   131  	}()
   132  
   133  	// Wait for remote repositories check completion
   134  	return rrc.waitForRemoteReposCheckCompletion(rtDetails, artifactoryUrl, progressBar)
   135  }
   136  
   137  func (rrc *RemoteRepositoryCheck) startCheckRemoteRepositories(rtDetails *httputils.HttpClientDetails, artifactoryUrl string, args RunArguments, requestBody []byte) (*progressbar.TasksProgressBar, error) {
   138  	var response *remoteUrlResponse
   139  	// Sometimes, POST api/plugins/execute/remoteRepositoriesCheck returns unexpectedly 404 errors, although the config-import plugin is installed.
   140  	// To overcome this issue, we use a custom retryExecutor and not the default retry executor that retries only on HTTP errors >= 500.
   141  	retryExecutor := clientutils.RetryExecutor{
   142  		Context:                  args.Context,
   143  		MaxRetries:               remoteUrlCheckRetries,
   144  		RetriesIntervalMilliSecs: remoteUrlCheckIntervalMilliSecs,
   145  		ErrorMessage:             fmt.Sprintf("Failed to start the remote repositories check in %s", artifactoryUrl),
   146  		LogMsgPrefix:             "[Config import]",
   147  		ExecutionHandler: func() (shouldRetry bool, err error) {
   148  			// Start the remote repositories check process
   149  			resp, responseBody, err := (*rrc.targetServicesManager).Client().SendPost(artifactoryUrl+utils.PluginsExecuteRestApi+"remoteRepositoriesCheck", requestBody, rtDetails)
   150  			if err != nil {
   151  				return false, err
   152  			}
   153  			if err = errorutils.CheckResponseStatusWithBody(resp, responseBody, http.StatusOK); err != nil {
   154  				return true, err
   155  			}
   156  
   157  			response, err = unmarshalRemoteUrlResponse(responseBody)
   158  			return false, err
   159  		},
   160  	}
   161  
   162  	if err := retryExecutor.Execute(); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	if args.ProgressMng == nil {
   167  		return nil, nil
   168  	}
   169  	return args.ProgressMng.NewTasksProgressBar(int64(response.TotalRepositories), coreutils.IsWindows(), "Remote repositories"), nil
   170  }
   171  
   172  func (rrc *RemoteRepositoryCheck) waitForRemoteReposCheckCompletion(rtDetails *httputils.HttpClientDetails, artifactoryUrl string, progressBar *progressbar.TasksProgressBar) (*[]inaccessibleRepository, error) {
   173  	pollingExecutor := &httputils.PollingExecutor{
   174  		Timeout:         remoteUrlCheckPollingTimeout,
   175  		PollingInterval: remoteUrlCheckPollingInterval,
   176  		MsgPrefix:       "Waiting for remote repositories check completion in Artifactory server at " + artifactoryUrl,
   177  		PollingAction:   rrc.createImportPollingAction(rtDetails, artifactoryUrl, progressBar),
   178  	}
   179  
   180  	body, err := pollingExecutor.Execute()
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	response, err := unmarshalRemoteUrlResponse(body)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	return &response.InaccessibleRepositories, nil
   189  }
   190  
   191  func (rrc *RemoteRepositoryCheck) createImportPollingAction(rtDetails *httputils.HttpClientDetails, artifactoryUrl string, progressBar *progressbar.TasksProgressBar) httputils.PollingAction {
   192  	return func() (shouldStop bool, responseBody []byte, err error) {
   193  		// Get config import status
   194  		resp, body, _, err := (*rrc.targetServicesManager).Client().SendGet(artifactoryUrl+utils.PluginsExecuteRestApi+"remoteRepositoriesCheckStatus", true, rtDetails)
   195  		if err != nil {
   196  			return true, nil, err
   197  		}
   198  
   199  		// 200 - Import completed
   200  		if resp.StatusCode == http.StatusOK {
   201  			return true, body, nil
   202  		}
   203  
   204  		// 202 - Update status
   205  		if resp.StatusCode == http.StatusAccepted {
   206  			response, err := unmarshalRemoteUrlResponse(body)
   207  			if err != nil {
   208  				return true, nil, err
   209  			}
   210  			if progressBar != nil {
   211  				delta := int64(response.CheckedRepositories) - progressBar.GetBar().Current()
   212  				progressBar.GetBar().IncrInt64(delta)
   213  			}
   214  		}
   215  
   216  		return false, nil, nil
   217  	}
   218  }
   219  
   220  // Unmarshal response from Artifactory to remoteUrlResponse
   221  func unmarshalRemoteUrlResponse(body []byte) (*remoteUrlResponse, error) {
   222  	log.Debug(fmt.Sprintf("Response from Artifactory:\n%s", body))
   223  	var response remoteUrlResponse
   224  	err := json.Unmarshal(body, &response)
   225  	return &response, errorutils.CheckError(err)
   226  }
   227  
   228  // Create csv summary of all the files with inaccessible remote repositories and log the result
   229  func handleFailureRun(inaccessibleRepositories []inaccessibleRepository) (err error) {
   230  	// Create summary
   231  	csvPath, err := utils.CreateCSVFile("inaccessible-repositories", inaccessibleRepositories, time.Now())
   232  	if err != nil {
   233  		log.Error("Couldn't create the inaccessible remote repository URLs CSV file", err)
   234  		return
   235  	}
   236  	// Log result
   237  	log.Info(fmt.Sprintf("Found %d inaccessible remote repository URLs. Check the summary CSV file in: %s", len(inaccessibleRepositories), csvPath))
   238  	return
   239  }