github.com/jfrog/jfrog-cli-core/v2@v2.51.0/general/envsetup/envsetup.go (about) 1 package envsetup 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "fmt" 7 "github.com/google/uuid" 8 "github.com/jfrog/jfrog-cli-core/v2/artifactory/commands/generic" 9 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 10 "github.com/jfrog/jfrog-cli-core/v2/common/commands" 11 "github.com/jfrog/jfrog-cli-core/v2/general" 12 "github.com/jfrog/jfrog-cli-core/v2/utils/config" 13 "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" 14 "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" 15 "github.com/jfrog/jfrog-client-go/access/services" 16 "github.com/jfrog/jfrog-client-go/http/httpclient" 17 clientUtils "github.com/jfrog/jfrog-client-go/utils" 18 "github.com/jfrog/jfrog-client-go/utils/errorutils" 19 ioUtils "github.com/jfrog/jfrog-client-go/utils/io" 20 "github.com/jfrog/jfrog-client-go/utils/io/httputils" 21 "github.com/jfrog/jfrog-client-go/utils/log" 22 "github.com/pkg/browser" 23 "net/http" 24 "time" 25 ) 26 27 type OutputFormat string 28 29 const ( 30 myJfrogEndPoint = "https://myjfrog-api.jfrog.com/api/v1/activation/cloud/cli/getStatus/" 31 syncSleepInterval = 5 * time.Second // 5 seconds 32 maxWaitMinutes = 30 * time.Minute // 30 minutes 33 34 // OutputFormat values 35 Human OutputFormat = "human" 36 Machine OutputFormat = "machine" 37 38 // When entering password on terminal the user has limited number of retries. 39 enterPasswordMaxRetries = 20 40 41 MessageIdes = "📦 If you're using VS Code, IntelliJ IDEA, WebStorm, PyCharm, Android Studio or GoLand\n" + 42 " Open the IDE 👉 Install the JFrog extension or plugin 👉 View the JFrog panel" 43 MessageDockerDesktop = "📦 Open Docker Desktop and install the JFrog Extension to scan any of your \n" + 44 " local docker images" 45 MessageDockerScan = "📦 Scan local Docker images from the terminal by running\n" + 46 " jf docker scan <image name>:<image tag>" 47 ) 48 49 type EnvSetupCommand struct { 50 registrationURL string 51 // In case encodedConnectionDetails were provided - we have a registered user that was invited to the platform. 52 encodedConnectionDetails string 53 id uuid.UUID 54 serverDetails *config.ServerDetails 55 progress ioUtils.ProgressMgr 56 outputFormat OutputFormat 57 } 58 59 func (ftc *EnvSetupCommand) SetRegistrationURL(registrationURL string) *EnvSetupCommand { 60 ftc.registrationURL = registrationURL 61 return ftc 62 } 63 64 func (ftc *EnvSetupCommand) SetEncodedConnectionDetails(encodedConnectionDetails string) *EnvSetupCommand { 65 ftc.encodedConnectionDetails = encodedConnectionDetails 66 return ftc 67 } 68 69 func (ftc *EnvSetupCommand) ServerDetails() (*config.ServerDetails, error) { 70 return nil, nil 71 } 72 73 func (ftc *EnvSetupCommand) SetProgress(progress ioUtils.ProgressMgr) { 74 ftc.progress = progress 75 } 76 77 func (ftc *EnvSetupCommand) SetOutputFormat(format OutputFormat) *EnvSetupCommand { 78 ftc.outputFormat = format 79 return ftc 80 } 81 82 func NewEnvSetupCommand() *EnvSetupCommand { 83 return &EnvSetupCommand{ 84 id: uuid.New(), 85 } 86 } 87 88 // This function is a wrapper around the 'ftc.progress.SetHeadlineMsg(msg)' API, 89 // to make sure that ftc.progress isn't nil. It can be nil in case the CI environment variable is set. 90 // In case ftc.progress is nil, the message sent will be prompted to the screen 91 // without the progress indication. 92 func (ftc *EnvSetupCommand) setHeadlineMsg(msg string) { 93 if ftc.progress != nil { 94 ftc.progress.SetHeadlineMsg(msg) 95 } else { 96 log.Output(msg + "...") 97 } 98 } 99 100 // This function is a wrapper around the 'ftc.progress.clearHeadlineMsg()' API, 101 // to make sure that ftc.progress isn't nil before clearing it. 102 // It can be nil in case the CI environment variable is set. 103 func (ftc *EnvSetupCommand) clearHeadlineMsg() { 104 if ftc.progress != nil { 105 ftc.progress.ClearHeadlineMsg() 106 } 107 } 108 109 // This function is a wrapper around the 'ftc.progress.Quit()' API, 110 // to make sure that ftc.progress isn't nil before clearing it. 111 // It can be nil in case the CI environment variable is set. 112 func (ftc *EnvSetupCommand) quitProgress() error { 113 if ftc.progress != nil { 114 return ftc.progress.Quit() 115 } 116 return nil 117 } 118 119 func (ftc *EnvSetupCommand) Run() (err error) { 120 err = ftc.SetupAndConfigServer() 121 if err != nil { 122 return 123 } 124 if ftc.outputFormat == Human { 125 // Closes the progress manger and reset the log prints. 126 err = ftc.quitProgress() 127 if err != nil { 128 return 129 } 130 log.Output() 131 log.Output(coreutils.PrintBold("Congrats! You're all set")) 132 log.Output("So what's next?") 133 message := 134 coreutils.PrintTitle("IDE") + "\n" + 135 MessageIdes + "\n\n" + 136 coreutils.PrintTitle("Docker") + "\n" + 137 "You can scan your local Docker images from the terminal or the Docker Desktop UI\n" + 138 MessageDockerScan + "\n" + 139 MessageDockerDesktop + "\n\n" + 140 coreutils.PrintTitle("Build, scan & deploy") + "\n" + 141 "1. 'cd' into your code project directory\n" + 142 "2. Run 'jf project init'\n\n" + 143 coreutils.PrintTitle("Read more") + "\n" + 144 "📦 Read more about how to get started at -\n" + 145 " " + coreutils.PrintLink(coreutils.GettingStartedGuideUrl) 146 err = coreutils.PrintTable("", "", message, false) 147 if err != nil { 148 return 149 } 150 if ftc.encodedConnectionDetails == "" { 151 log.Output(coreutils.PrintBold("Important")) 152 log.Output("Please use the email we've just sent you, to verify your email address during the next 48 hours.\n") 153 } 154 } 155 return 156 } 157 158 func (ftc *EnvSetupCommand) SetupAndConfigServer() (err error) { 159 var server *config.ServerDetails 160 // If credentials were provided, this means that the user was invited to join an existing JFrog environment. 161 // Otherwise, this is a brand-new user, that needs to register and set up a new JFrog environment. 162 if ftc.encodedConnectionDetails == "" { 163 server, err = ftc.setupNewUser() 164 } else { 165 server, err = ftc.setupExistingUser() 166 } 167 if err != nil { 168 return 169 } 170 err = general.ConfigServerWithDeducedId(server, false, false) 171 return 172 } 173 174 func (ftc *EnvSetupCommand) setupNewUser() (server *config.ServerDetails, err error) { 175 if ftc.outputFormat == Human { 176 ftc.setHeadlineMsg("Just fill out its details in your browser 📝") 177 time.Sleep(8 * time.Second) 178 } else { 179 // Closes the progress manger and reset the log prints. 180 err = ftc.quitProgress() 181 if err != nil { 182 return 183 } 184 } 185 err = browser.OpenURL(ftc.registrationURL + "?id=" + ftc.id.String()) 186 if err != nil { 187 return 188 } 189 server, err = ftc.getNewServerDetails() 190 return 191 } 192 193 func (ftc *EnvSetupCommand) setupExistingUser() (server *config.ServerDetails, err error) { 194 err = ftc.quitProgress() 195 if err != nil { 196 return 197 } 198 server, err = ftc.decodeConnectionDetails() 199 if err != nil { 200 return 201 } 202 if server.Url == "" { 203 err = errorutils.CheckErrorf("The response from JFrog Access does not include a JFrog environment URL") 204 return 205 } 206 if server.AccessToken != "" { 207 // If the server details received from JFrog Access include an access token, this access token is 208 // short-lived, and we therefore need to replace it with a new long-lived access token, and configure 209 // JFrog CLI with it. 210 err = GenerateNewLongTermRefreshableAccessToken(server) 211 return 212 } 213 if server.User == "" { 214 err = errorutils.CheckErrorf("The response from JFrog Access does not includes a username or access token") 215 return 216 } 217 // Url and accessToken/userName must be provided in the base64 encoded connection details. 218 // APIkey/password are optional - In case they were not provided user can enter his password on console. 219 // Password will be validated before the config command is being called. 220 if server.Password == "" { 221 err = ftc.scanAndValidateJFrogPasswordFromConsole(server) 222 } 223 return 224 } 225 226 func (ftc *EnvSetupCommand) scanAndValidateJFrogPasswordFromConsole(server *config.ServerDetails) (err error) { 227 // User has limited number of retries to enter his correct password. 228 // Password validation is operated by Artifactory ping API. 229 server.ArtifactoryUrl = clientUtils.AddTrailingSlashIfNeeded(server.Url) + "artifactory/" 230 for i := 0; i < enterPasswordMaxRetries; i++ { 231 server.Password, err = ioutils.ScanJFrogPasswordFromConsole() 232 if err != nil { 233 return 234 } 235 // Validate correct password by using Artifactory ping API. 236 pingCmd := generic.NewPingCommand().SetServerDetails(server) 237 err = commands.Exec(pingCmd) 238 if err == nil { 239 // No error while encrypting password => correct password. 240 return nil 241 } 242 log.Output(err.Error()) 243 } 244 err = errorutils.CheckErrorf("bad credentials: Wrong password. ") 245 return 246 } 247 248 // Take the short-lived token and generate a long term (1 year expiry) refreshable accessToken. 249 func GenerateNewLongTermRefreshableAccessToken(server *config.ServerDetails) (err error) { 250 accessManager, err := utils.CreateAccessServiceManager(server, false) 251 if err != nil { 252 return 253 } 254 // Create refreshable accessToken with 1 year expiry from the given short expiry token. 255 params := createLongExpirationRefreshableTokenParams() 256 token, err := accessManager.CreateAccessToken(*params) 257 if err != nil { 258 return 259 } 260 server.AccessToken = token.AccessToken 261 server.RefreshToken = token.RefreshToken 262 return 263 } 264 265 func createLongExpirationRefreshableTokenParams() *services.CreateTokenParams { 266 params := services.CreateTokenParams{} 267 // Using the platform's default expiration (1 year by default). 268 params.ExpiresIn = nil 269 params.Refreshable = clientUtils.Pointer(true) 270 params.Audience = "*@*" 271 return ¶ms 272 } 273 274 func (ftc *EnvSetupCommand) decodeConnectionDetails() (server *config.ServerDetails, err error) { 275 rawDecodedText, err := base64.StdEncoding.DecodeString(ftc.encodedConnectionDetails) 276 if errorutils.CheckError(err) != nil { 277 return 278 } 279 err = json.Unmarshal(rawDecodedText, &server) 280 if errorutils.CheckError(err) != nil { 281 return nil, err 282 } 283 return 284 } 285 286 func (ftc *EnvSetupCommand) CommandName() string { 287 return "setup" 288 } 289 290 // Returns the new server details from My-JFrog 291 func (ftc *EnvSetupCommand) getNewServerDetails() (serverDetails *config.ServerDetails, err error) { 292 requestBody := &myJfrogGetStatusRequest{CliRegistrationId: ftc.id.String()} 293 requestContent, err := json.Marshal(requestBody) 294 if errorutils.CheckError(err) != nil { 295 return nil, err 296 } 297 298 httpClientDetails := httputils.HttpClientDetails{ 299 Headers: map[string]string{"Content-Type": "application/json"}, 300 } 301 client, err := httpclient.ClientBuilder().Build() 302 if err != nil { 303 return nil, err 304 } 305 306 // Define the MyJFrog polling logic. 307 pollingMessage := fmt.Sprintf("Sync: Get MyJFrog status report. Request ID:%s...", ftc.id) 308 pollingErrorMessage := "Sync: Get MyJFrog status request failed. Attempt: %d. Error: %s" 309 // The max consecutive polling errors allowed, before completely failing the setup action. 310 const maxConsecutiveErrors = 6 311 errorsCount := 0 312 readyMessageDisplayed := false 313 pollingAction := func() (shouldStop bool, responseBody []byte, err error) { 314 log.Debug(pollingMessage) 315 // Send request to MyJFrog. 316 resp, body, err := client.SendPost(myJfrogEndPoint, requestContent, httpClientDetails, "") 317 // If an HTTP error occurred. 318 if err != nil { 319 errorsCount++ 320 log.Debug(fmt.Sprintf(pollingErrorMessage, errorsCount, err.Error())) 321 if errorsCount == maxConsecutiveErrors { 322 return true, nil, err 323 } 324 return false, nil, nil 325 } 326 // If the response is not the expected 200 or 404. 327 if err = errorutils.CheckResponseStatusWithBody(resp, body, http.StatusOK, http.StatusNotFound); err != nil { 328 errorsCount++ 329 log.Debug(fmt.Sprintf(pollingErrorMessage, errorsCount, err.Error())) 330 if errorsCount == maxConsecutiveErrors { 331 return true, nil, err 332 } 333 return false, nil, nil 334 } 335 errorsCount = 0 336 337 // Wait for 'ready=true' response from MyJFrog 338 if resp.StatusCode == http.StatusOK { 339 if !readyMessageDisplayed { 340 if ftc.outputFormat == Machine { 341 log.Output("PREPARING_ENV") 342 } else { 343 ftc.clearHeadlineMsg() 344 ftc.setHeadlineMsg("Almost done! Please hang on while JFrog CLI completes the setup 🛠") 345 } 346 readyMessageDisplayed = true 347 } 348 statusResponse := myJfrogGetStatusResponse{} 349 if err = json.Unmarshal(body, &statusResponse); err != nil { 350 return true, nil, err 351 } 352 // Got the new server details 353 if statusResponse.Ready { 354 return true, body, nil 355 } 356 } 357 // The expected 404 response or 200 response without 'Ready' 358 return false, nil, nil 359 } 360 361 pollingExecutor := &httputils.PollingExecutor{ 362 Timeout: maxWaitMinutes, 363 PollingInterval: syncSleepInterval, 364 PollingAction: pollingAction, 365 } 366 367 body, err := pollingExecutor.Execute() 368 if err != nil { 369 return nil, err 370 } 371 statusResponse := myJfrogGetStatusResponse{} 372 if err = json.Unmarshal(body, &statusResponse); err != nil { 373 return nil, errorutils.CheckError(err) 374 } 375 ftc.clearHeadlineMsg() 376 serverDetails = &config.ServerDetails{ 377 Url: statusResponse.PlatformUrl, 378 AccessToken: statusResponse.AccessToken, 379 } 380 ftc.serverDetails = serverDetails 381 return serverDetails, nil 382 } 383 384 type myJfrogGetStatusRequest struct { 385 CliRegistrationId string `json:"cliRegistrationId,omitempty"` 386 } 387 388 type myJfrogGetStatusResponse struct { 389 CliRegistrationId string `json:"cliRegistrationId,omitempty"` 390 Ready bool `json:"ready,omitempty"` 391 AccessToken string `json:"accessToken,omitempty"` 392 PlatformUrl string `json:"platformUrl,omitempty"` 393 }