internal/cloud/endpoints.go (172 lines of code) (raw):

/* * Copyright 2021-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cloud import ( "bytes" "errors" "fmt" "io" "net/http" "net/url" "slices" "time" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" log "github.com/sirupsen/logrus" ) const ( DefaultEndpoint = "https://qodana.cloud" defaultNumberOfRetries = 3 defaultCooldownTimeSeconds = 30 defaultRequestTimeout = 30 ) // QdRootEndpoint contains scheme, hostname and port type QdRootEndpoint struct { Url string } type QdApiEndpoints struct { RootEndpoint *QdRootEndpoint LintersApiUrl string CloudApiUrl string } type QdClient struct { apiUrl string httpClient *http.Client token string } var endpoint *QdRootEndpoint var endpointApis *QdApiEndpoints func GetCloudApiEndpoints() *QdApiEndpoints { if endpointApis == nil { apis, err := GetCloudRootEndpoint().requestApiEndpoints() if err != nil { log.Fatalf("Failed to obtain proper API endpoints: %v", err) } endpointApis = apis } return endpointApis } func GetCloudRootEndpoint() *QdRootEndpoint { if endpoint != nil { return endpoint } userUrl := qdenv.GetQodanaGlobalEnv(qdenv.QodanaEndpointEnv) if userUrl == "" { userUrl = DefaultEndpoint } host, err := parseRawURL(userUrl) if err != nil { log.Fatal(err) } endpoint = &QdRootEndpoint{host} return endpoint } func parseRawURL(rawUrl string) (host string, err error) { parsedUrl, err := url.ParseRequestURI(rawUrl) if err != nil || parsedUrl.Host == "" { parsedUrl, repErr := url.ParseRequestURI("https://" + rawUrl) if repErr != nil { return "", err } return fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host), nil } return fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host), nil } func (endpoints *QdApiEndpoints) NewCloudApiClient(token string) *QdClient { return &QdClient{ httpClient: &http.Client{ Timeout: getRequestTimeout(), }, apiUrl: endpoints.CloudApiUrl, token: token, } } func getRequestTimeout() time.Duration { return time.Duration(GetEnvWithDefaultInt(qdenv.QodanaCloudRequestTimeoutEnv, defaultRequestTimeout)) * time.Second } func (endpoints *QdApiEndpoints) NewLintersApiClient(token string) *QdClient { return &QdClient{ httpClient: &http.Client{ Timeout: getRequestTimeout(), }, apiUrl: endpoints.LintersApiUrl, token: token, } } type APIError struct { StatusCode int Message string } func (e *APIError) Error() string { return fmt.Sprintf("response code '%d', message '%s'", e.StatusCode, e.Message) } type QdCloudRequest struct { Path string Method string Headers map[string]string Body []byte AcceptedStatuses []int Retries int Cooldown int } func NewCloudRequest(path string) QdCloudRequest { return QdCloudRequest{ Path: path, Method: "GET", AcceptedStatuses: []int{http.StatusUnauthorized, http.StatusNotFound}, Retries: GetEnvWithDefaultInt(qdenv.QodanaCloudRequestRetriesEnv, defaultNumberOfRetries), Cooldown: GetEnvWithDefaultInt(qdenv.QodanaCloudRequestCooldownEnv, defaultCooldownTimeSeconds), } } func (client *QdClient) doRequest(request *QdCloudRequest) ([]byte, error) { var response []byte var err error for i := 1; i <= request.Retries; i++ { response, err = client.doRequestAttempt(request) if err == nil { return response, nil } var versionError *APIError if errors.As(err, &versionError) { if slices.Contains(request.AcceptedStatuses, versionError.StatusCode) { return nil, err // return if accepted status code, like 401 } } log.Errorf("Attempt #%d of %d for request to '%s' failed. Error: %v", i, request.Retries, request.Path, err) if i < request.Retries { log.Printf("Next attempt in %d seconds", request.Cooldown) time.Sleep(time.Duration(request.Cooldown) * time.Second) } } return response, errors.New("failed to obtain proper cloud response") } func (client *QdClient) doRequestAttempt(request *QdCloudRequest) ([]byte, error) { requestUrl := client.apiUrl + request.Path var resp *http.Response var responseErr error req, err := http.NewRequest(request.Method, requestUrl, bytes.NewBuffer(request.Body)) if err != nil { return nil, err } if client.token != "" { req.Header.Set("Authorization", "Bearer "+client.token) } req.Header.Set("Content-Type", "application/json") for key, value := range request.Headers { req.Header.Set(key, value) } resp, responseErr = client.httpClient.Do(req) if responseErr != nil { return nil, responseErr } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { log.Fatal(err) } }(resp.Body) responseBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { return responseBody, nil } return nil, &APIError{StatusCode: resp.StatusCode, Message: string(responseBody)} }