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 }