github.com/sharovik/devbot@v1.0.1-0.20240308094637-4a0387c40516/internal/client/bitbucket.go (about) 1 package client 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "time" 10 11 _time "github.com/sharovik/devbot/internal/service/time" 12 13 "github.com/sharovik/devbot/internal/dto" 14 "github.com/sharovik/devbot/internal/log" 15 ) 16 17 // BitBucketClient the bitbucket client struct 18 type BitBucketClient struct { 19 client BaseHTTPClientInterface 20 OauthToken string 21 OauthTokenExpire time.Time 22 RefreshToken string 23 } 24 25 type responseAccessToken struct { 26 AccessToken string `json:"access_token"` 27 Scopes string `json:"scopes"` 28 ExpiresIn int `json:"expires_in"` 29 RefreshToken string `json:"refresh_token"` 30 TokenType string `json:"token_type"` 31 } 32 33 const ( 34 //DefaultBitBucketBaseAPIUrl the base url 35 DefaultBitBucketBaseAPIUrl = "https://api.bitbucket.org/2.0" 36 37 //DefaultBitBucketAccessTokenURL the access token endpoint which will be used for token generation 38 DefaultBitBucketAccessTokenURL = "https://bitbucket.org/site/oauth2" 39 40 //DefaultBitBucketMainBranch the default main branch 41 DefaultBitBucketMainBranch = "master" 42 43 //ErrorBranchExists error message for "branch exists error" 44 ErrorBranchExists = "BRANCH_ALREADY_EXISTS" 45 46 //StrategySquash the squash strategy which can be used during the merge 47 StrategySquash = "squash" 48 49 //StrategyMerge the default merge strategy which can be used during the merge 50 StrategyMerge = "merge_commit" 51 52 //ErrorMsgNoAccess error message response of bot, once he got a bad status code from the API 53 ErrorMsgNoAccess = "received unauthorized response. Looks like I'm not permitted to do any actions to that repository" 54 ) 55 56 // Init initialise the client 57 func (b *BitBucketClient) Init(client BaseHTTPClientInterface) { 58 b.client = client 59 } 60 61 func (b *BitBucketClient) isTokenInvalid() bool { 62 if b.OauthToken == "" { 63 log.Logger().Warn().Str("error", "oauth_empty").Msg("Invalid token") 64 return true 65 } 66 67 if b.OauthTokenExpire.Unix() <= _time.Service.Now().Unix() { 68 log.Logger().Warn().Time("time", b.OauthTokenExpire).Str("error", "expired").Msg("Invalid token") 69 return true 70 } 71 72 return false 73 } 74 75 func (b *BitBucketClient) beforeRequest() error { 76 log.Logger().StartMessage("Before BitBucket request") 77 if b.isTokenInvalid() { 78 log.Logger().Warn().Msg("Trying to regenerate the token") 79 if err := b.loadAuthToken(); err != nil { 80 log.Logger().AddError(err).Msg("Failed to generate new token") 81 log.Logger().FinishMessage("Before BitBucket request") 82 return err 83 } 84 } 85 86 log.Logger().FinishMessage("Before BitBucket request") 87 return nil 88 } 89 90 // GetAuthToken method retrieves the access token which can be used for custom needs outside of the internal services 91 func (b *BitBucketClient) GetAuthToken() (string, error) { 92 if err := b.beforeRequest(); err != nil { 93 log.Logger().FinishMessage("Can't load access token") 94 return "", err 95 } 96 97 return b.OauthToken, nil 98 } 99 100 func (b *BitBucketClient) loadAuthToken() error { 101 log.Logger().StartMessage("Loading OAuth token") 102 103 b.client.SetBaseURL(DefaultBitBucketAccessTokenURL) 104 b.client.SetOauthToken("") //this will cleanup the token in the client and will generate new basic auth for specific client_id and client_secret 105 106 formData := url.Values{} 107 formData.Add("grant_type", "client_credentials") 108 109 response, _, err := b.client.Post("/access_token", formData.Encode(), map[string]string{ 110 "Content-Type": "application/x-www-form-urlencoded;", 111 "Authorization": fmt.Sprintf("Basic %s", b.client.BasicAuth( 112 b.client.GetClientID(), 113 b.client.GetClientSecret(), 114 )), 115 }) 116 if err != nil { 117 return err 118 } 119 120 var responseObject responseAccessToken 121 err = json.Unmarshal(response, &responseObject) 122 if err != nil { 123 return err 124 } 125 126 b.RefreshToken = responseObject.RefreshToken 127 b.OauthTokenExpire = _time.Service.Now().Add(time.Second * time.Duration(responseObject.ExpiresIn)) 128 b.OauthToken = responseObject.AccessToken 129 b.client.SetOauthToken(responseObject.AccessToken) 130 131 log.Logger().FinishMessage("Loading OAuth token") 132 return nil 133 } 134 135 // CreateBranch creates the branch in API 136 func (b *BitBucketClient) CreateBranch(workspace string, repositorySlug string, branchName string) (dto.BitBucketResponseBranchCreate, error) { 137 log.Logger().StartMessage("Create branch") 138 if err := b.beforeRequest(); err != nil { 139 log.Logger().FinishMessage("Create branch") 140 return dto.BitBucketResponseBranchCreate{}, err 141 } 142 143 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 144 145 endpoint := fmt.Sprintf("/repositories/%s/%s/refs/branches/%s", workspace, repositorySlug, branchName) 146 response, statusCode, err := b.client.Get(endpoint, map[string]string{}) 147 if err != nil { 148 log.Logger().FinishMessage("Create branch") 149 return dto.BitBucketResponseBranchCreate{}, err 150 } 151 152 responseObject := dto.BitBucketResponseBranchCreate{} 153 if statusCode == http.StatusNotFound { 154 log.Logger().Info().Str("branch", branchName).Msg("Release branch wasn't found. Trying to create it.") 155 request := dto.BitBucketRequestBranchCreate{ 156 Name: branchName, 157 Target: dto.BitBucketBranchTarget{ 158 Hash: DefaultBitBucketMainBranch, 159 }, 160 } 161 162 byteRequest, err := json.Marshal(request) 163 if err != nil { 164 log.Logger().FinishMessage("Create branch") 165 return dto.BitBucketResponseBranchCreate{}, err 166 } 167 168 endpoint := fmt.Sprintf("/repositories/%s/%s/refs/branches", workspace, repositorySlug) 169 response, statusCode, err := b.client.Post(endpoint, byteRequest, map[string]string{}) 170 if err != nil { 171 log.Logger().AddError(err). 172 Msg("Failed to trigger request") 173 log.Logger().FinishMessage("Create branch") 174 175 return dto.BitBucketResponseBranchCreate{}, err 176 } 177 178 if err := json.Unmarshal(response, &responseObject); err != nil { 179 log.Logger().Info(). 180 Str("branch", branchName). 181 Int("status_code", statusCode). 182 Str("response", string(response)). 183 Msg("Failed to unmarshal response") 184 log.Logger().FinishMessage("Create branch") 185 return dto.BitBucketResponseBranchCreate{}, err 186 } 187 188 if statusCode == http.StatusBadRequest { 189 if responseObject.Data.Key == ErrorBranchExists { 190 log.Logger().Info(). 191 Str("branch", branchName). 192 Int("status_code", statusCode). 193 RawJSON("response", response). 194 Msg("Branch already exists") 195 log.Logger().FinishMessage("Create branch") 196 return responseObject, nil 197 } 198 } 199 200 if statusCode != http.StatusCreated { 201 log.Logger().Warn(). 202 Str("branch", branchName). 203 Int("status_code", statusCode). 204 Interface("response", responseObject). 205 Msg("Bad status code received") 206 207 log.Logger().FinishMessage("Create branch") 208 return dto.BitBucketResponseBranchCreate{}, errors.New("wrong status code received during the branch creation. See the logs for more information. ") 209 } 210 211 log.Logger().Info(). 212 Str("branch", branchName). 213 Int("status_code", statusCode). 214 RawJSON("response", response).Msg("Create branch result") 215 log.Logger().FinishMessage("Create branch") 216 return responseObject, nil 217 } 218 219 err = json.Unmarshal(response, &responseObject) 220 if err != nil { 221 log.Logger().AddError(err).Msg("Error during response unmarshal") 222 log.Logger().FinishMessage("Create branch") 223 return dto.BitBucketResponseBranchCreate{}, err 224 } 225 226 log.Logger().FinishMessage("Create branch") 227 return responseObject, nil 228 } 229 230 // GetBranch creates the branch in API 231 func (b *BitBucketClient) GetBranch(workspace string, repositorySlug string, branchName string) (dto.BitBucketResponseBranchCreate, error) { 232 log.Logger().StartMessage("Get branch") 233 if err := b.beforeRequest(); err != nil { 234 log.Logger().FinishMessage("Get branch") 235 return dto.BitBucketResponseBranchCreate{}, err 236 } 237 238 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 239 240 endpoint := fmt.Sprintf("/repositories/%s/%s/refs/branches/%s", workspace, repositorySlug, branchName) 241 response, statusCode, err := b.client.Get(endpoint, map[string]string{}) 242 if err != nil { 243 log.Logger().FinishMessage("Get branch") 244 return dto.BitBucketResponseBranchCreate{}, err 245 } 246 247 if statusCode == http.StatusNotFound { 248 log.Logger().FinishMessage("Get branch") 249 return dto.BitBucketResponseBranchCreate{}, errors.New("this branch doesn't exist. ") 250 } 251 252 if statusCode == http.StatusForbidden { 253 log.Logger().FinishMessage("Get branch") 254 return dto.BitBucketResponseBranchCreate{}, errors.New("action is not permitted. ") 255 } 256 257 responseObject := dto.BitBucketResponseBranchCreate{} 258 err = json.Unmarshal(response, &responseObject) 259 if err != nil { 260 log.Logger().AddError(err).Msg("Error during response unmarshal") 261 log.Logger().FinishMessage("Get branch") 262 return dto.BitBucketResponseBranchCreate{}, err 263 } 264 265 log.Logger().FinishMessage("Get branch") 266 return responseObject, nil 267 } 268 269 // PullRequestInfo gets the pull-requests information 270 func (b *BitBucketClient) PullRequestInfo(workspace string, repositorySlug string, pullRequestID int64) (dto.BitBucketPullRequestInfoResponse, error) { 271 log.Logger().StartMessage("Get pull-request status") 272 if err := b.beforeRequest(); err != nil { 273 log.Logger().FinishMessage("Get pull-request status") 274 return dto.BitBucketPullRequestInfoResponse{}, err 275 } 276 277 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 278 endpoint := fmt.Sprintf("/repositories/%s/%s/pullrequests/%d", workspace, repositorySlug, pullRequestID) 279 response, _, err := b.client.Get(endpoint, map[string]string{}) 280 281 if err != nil { 282 log.Logger().FinishMessage("Get pull-request status") 283 return dto.BitBucketPullRequestInfoResponse{}, err 284 } 285 286 var responseObject dto.BitBucketPullRequestInfoResponse 287 err = json.Unmarshal(response, &responseObject) 288 if err != nil { 289 log.Logger().FinishMessage("Get pull-request status") 290 return dto.BitBucketPullRequestInfoResponse{}, err 291 } 292 293 log.Logger().FinishMessage("Get pull-request status") 294 return responseObject, nil 295 } 296 297 // MergePullRequest merge the selected pull-request 298 func (b *BitBucketClient) MergePullRequest(workspace string, repositorySlug string, pullRequestID int64, description string, strategy string) (dto.BitBucketPullRequestInfoResponse, error) { 299 log.Logger().StartMessage("Merge pull-request") 300 if err := b.beforeRequest(); err != nil { 301 log.Logger().FinishMessage("Merge pull-request") 302 return dto.BitBucketPullRequestInfoResponse{}, err 303 } 304 305 formData := map[string]string{ 306 "merge_strategy": strategy, 307 "message": description, 308 "close_source_branch": "1", 309 } 310 311 byteString, err := json.Marshal(formData) 312 if err != nil { 313 log.Logger().FinishMessage("Merge pull-request") 314 return dto.BitBucketPullRequestInfoResponse{}, err 315 } 316 317 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 318 endpoint := fmt.Sprintf("/repositories/%s/%s/pullrequests/%d/merge?async=false", workspace, repositorySlug, pullRequestID) 319 320 var dtoResponse = dto.BitBucketPullRequestInfoResponse{} 321 response, statusCode, err := b.client.Post(endpoint, byteString, map[string]string{}) 322 if err != nil { 323 log.Logger().FinishMessage("Merge pull-request") 324 return dto.BitBucketPullRequestInfoResponse{}, err 325 } 326 327 //In that case the bitbucket accepts our request and the Pull-request will be merged but in async way(even if we specified async=false) 328 if statusCode == http.StatusAccepted { 329 log.Logger(). 330 Debug(). 331 RawJSON("response", response). 332 Int("status_code", statusCode). 333 Msg("The response with poll link received.") 334 log.Logger().FinishMessage("Merge pull-request") 335 return dtoResponse, nil 336 } 337 338 if err := json.Unmarshal(response, &dtoResponse); err != nil { 339 log.Logger(). 340 AddError(err). 341 RawJSON("response", response). 342 Int("status_code", statusCode). 343 Msg("Error during the request unmarshal.") 344 log.Logger().FinishMessage("Merge pull-request") 345 return dto.BitBucketPullRequestInfoResponse{}, err 346 } 347 348 if statusCode == http.StatusBadRequest { 349 log.Logger().FinishMessage("Merge pull-request") 350 return dto.BitBucketPullRequestInfoResponse{}, fmt.Errorf("bitbucket response with the error: %s", dtoResponse.Error.Message) 351 } 352 353 if statusCode == http.StatusUnauthorized { 354 log.Logger().FinishMessage("Merge pull-request") 355 return dto.BitBucketPullRequestInfoResponse{}, fmt.Errorf(ErrorMsgNoAccess) 356 } 357 358 if statusCode == http.StatusNotFound { 359 log.Logger().FinishMessage("Merge pull-request") 360 return dto.BitBucketPullRequestInfoResponse{}, errors.New("selected pull-request was not found :( ") 361 } 362 363 log.Logger().FinishMessage("Merge pull-request") 364 return dtoResponse, nil 365 } 366 367 // ChangePullRequestDestination changes the pull-request destination to selected one 368 func (b *BitBucketClient) ChangePullRequestDestination(workspace string, repositorySlug string, pullRequestID int64, title string, branchName string) (dto.BitBucketPullRequestInfoResponse, error) { 369 log.Logger().StartMessage("Change destination") 370 if err := b.beforeRequest(); err != nil { 371 log.Logger().FinishMessage("Change destination") 372 return dto.BitBucketPullRequestInfoResponse{}, err 373 } 374 375 byteString, err := json.Marshal(dto.BitBucketPullRequestDestinationUpdateRequest{ 376 Title: title, 377 Destination: dto.BitBucketPullRequestDestination{ 378 Branch: dto.BitBucketPullRequestDestinationBranch{ 379 Name: branchName, 380 }, 381 }, 382 }) 383 384 if err != nil { 385 log.Logger().FinishMessage("Change destination") 386 return dto.BitBucketPullRequestInfoResponse{}, err 387 } 388 389 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 390 endpoint := fmt.Sprintf("/repositories/%s/%s/pullrequests/%d", workspace, repositorySlug, pullRequestID) 391 392 var dtoResponse = dto.BitBucketPullRequestInfoResponse{} 393 response, statusCode, err := b.client.Put(endpoint, byteString, map[string]string{}) 394 if err != nil { 395 log.Logger().FinishMessage("Change destination") 396 return dto.BitBucketPullRequestInfoResponse{}, err 397 } 398 399 if err := json.Unmarshal(response, &dtoResponse); err != nil { 400 log.Logger().FinishMessage("Change destination") 401 return dto.BitBucketPullRequestInfoResponse{}, err 402 } 403 404 if statusCode == http.StatusBadRequest { 405 log.Logger().FinishMessage("Change destination") 406 return dto.BitBucketPullRequestInfoResponse{}, errors.New(dtoResponse.Error.Message) 407 } 408 409 if statusCode == http.StatusUnauthorized { 410 log.Logger().FinishMessage("Change destination") 411 return dto.BitBucketPullRequestInfoResponse{}, errors.New(ErrorMsgNoAccess) 412 } 413 414 if statusCode == http.StatusNotFound { 415 log.Logger().FinishMessage("Change destination") 416 return dto.BitBucketPullRequestInfoResponse{}, errors.New("selected pull-request was not found :( ") 417 } 418 419 var responseObject dto.BitBucketPullRequestInfoResponse 420 err = json.Unmarshal(response, &responseObject) 421 if err != nil { 422 log.Logger().FinishMessage("Change destination") 423 return dto.BitBucketPullRequestInfoResponse{}, err 424 } 425 426 log.Logger().FinishMessage("Change destination") 427 return responseObject, nil 428 } 429 430 // CreatePullRequest creates the pull-request 431 func (b *BitBucketClient) CreatePullRequest(workspace string, repositorySlug string, request dto.BitBucketRequestPullRequestCreate) (dto.BitBucketPullRequestInfoResponse, error) { 432 log.Logger().StartMessage("Create pull-request") 433 if err := b.beforeRequest(); err != nil { 434 log.Logger().FinishMessage("Create pull-request") 435 return dto.BitBucketPullRequestInfoResponse{}, err 436 } 437 438 byteString, err := json.Marshal(request) 439 if err != nil { 440 log.Logger().FinishMessage("Create pull-request") 441 return dto.BitBucketPullRequestInfoResponse{}, err 442 } 443 444 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 445 endpoint := fmt.Sprintf("/repositories/%s/%s/pullrequests", workspace, repositorySlug) 446 447 response, statusCode, err := b.client.Post(endpoint, byteString, map[string]string{}) 448 if err != nil { 449 log.Logger().AddError(err).Msg("Error during the request") 450 log.Logger().FinishMessage("Create pull-request") 451 return dto.BitBucketPullRequestInfoResponse{}, err 452 } 453 454 var dtoResponse = dto.BitBucketPullRequestInfoResponse{} 455 if err := json.Unmarshal(response, &dtoResponse); err != nil { 456 log.Logger().AddError(err).Msg("Error during the unmarshal") 457 log.Logger().FinishMessage("Create pull-request") 458 return dto.BitBucketPullRequestInfoResponse{}, err 459 } 460 461 if statusCode == http.StatusBadRequest { 462 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Bad request status code") 463 log.Logger().FinishMessage("Create pull-request") 464 return dto.BitBucketPullRequestInfoResponse{}, errors.New(dtoResponse.Error.Message) 465 } 466 467 if statusCode == http.StatusUnauthorized { 468 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Unauthorized status code") 469 log.Logger().FinishMessage("Create pull-request") 470 return dto.BitBucketPullRequestInfoResponse{}, errors.New(ErrorMsgNoAccess) 471 } 472 473 if statusCode == http.StatusNotFound { 474 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Not found status code") 475 log.Logger().FinishMessage("Create pull-request") 476 return dto.BitBucketPullRequestInfoResponse{}, errors.New("endpoint or selected branch was not found :( ") 477 } 478 479 log.Logger().FinishMessage("Create pull-request") 480 return dtoResponse, nil 481 } 482 483 // RunPipeline runs the selected custom pipeline 484 func (b *BitBucketClient) RunPipeline(workspace string, repositorySlug string, request dto.BitBucketRequestRunPipeline) (dto.BitBucketResponseRunPipeline, error) { 485 log.Logger().StartMessage("Run pipeline") 486 if err := b.beforeRequest(); err != nil { 487 log.Logger().FinishMessage("Run pipeline") 488 return dto.BitBucketResponseRunPipeline{}, err 489 } 490 491 if len(request.Variables) == 0 { 492 request.Variables = []dto.Variable{} 493 } 494 495 byteString, err := json.Marshal(request) 496 if err != nil { 497 log.Logger().FinishMessage("Run pipeline") 498 return dto.BitBucketResponseRunPipeline{}, err 499 } 500 501 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 502 endpoint := fmt.Sprintf("/repositories/%s/%s/pipelines/", workspace, repositorySlug) 503 response, statusCode, err := b.client.Post(endpoint, byteString, map[string]string{}) 504 if err != nil { 505 log.Logger().FinishMessage("Run pipeline") 506 return dto.BitBucketResponseRunPipeline{}, err 507 } 508 509 fmt.Println(string(response)) 510 var responseObject dto.BitBucketResponseRunPipeline 511 err = json.Unmarshal(response, &responseObject) 512 if err != nil { 513 log.Logger().FinishMessage("Run pipeline") 514 return dto.BitBucketResponseRunPipeline{}, err 515 } 516 517 if statusCode == http.StatusBadRequest { 518 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Bad request status code") 519 log.Logger().FinishMessage("Run pipeline") 520 return dto.BitBucketResponseRunPipeline{}, fmt.Errorf("%s. (%s)", responseObject.Error.Message, responseObject.Error.Detail) 521 } 522 523 if statusCode == http.StatusUnauthorized { 524 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Unauthorized status code") 525 log.Logger().FinishMessage("Run pipeline") 526 return dto.BitBucketResponseRunPipeline{}, errors.New(ErrorMsgNoAccess) 527 } 528 529 if statusCode == http.StatusNotFound { 530 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Not found status code") 531 log.Logger().FinishMessage("Run pipeline") 532 return dto.BitBucketResponseRunPipeline{}, errors.New("endpoint or selected branch was not found :( ") 533 } 534 535 log.Logger().FinishMessage("Run pipeline") 536 return responseObject, nil 537 } 538 539 // GetDefaultReviewers gets default reviewers 540 func (b *BitBucketClient) GetDefaultReviewers(workspace string, repositorySlug string) (dto.BitBucketResponseDefaultReviewers, error) { 541 if err := b.beforeRequest(); err != nil { 542 return dto.BitBucketResponseDefaultReviewers{}, err 543 } 544 545 b.client.SetBaseURL(DefaultBitBucketBaseAPIUrl) 546 endpoint := fmt.Sprintf("/repositories/%s/%s/default-reviewers", workspace, repositorySlug) 547 548 response, statusCode, err := b.client.Get(endpoint, map[string]string{}) 549 if err != nil { 550 log.Logger(). 551 AddError(err). 552 RawJSON("response", response). 553 Int("status_code", statusCode). 554 Msg("Error during the request.") 555 return dto.BitBucketResponseDefaultReviewers{}, err 556 } 557 558 var dtoResponse = dto.BitBucketResponseDefaultReviewers{} 559 if err := json.Unmarshal(response, &dtoResponse); err != nil { 560 log.Logger().AddError(err).Msg("Error during the unmarshal") 561 return dto.BitBucketResponseDefaultReviewers{}, err 562 } 563 564 if statusCode == http.StatusUnauthorized { 565 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Unauthorized status code") 566 return dto.BitBucketResponseDefaultReviewers{}, errors.New(ErrorMsgNoAccess) 567 } 568 569 if statusCode == http.StatusNotFound { 570 log.Logger().Warn().Int("status_code", statusCode).Str("response", string(response)).Msg("Not found status code") 571 return dto.BitBucketResponseDefaultReviewers{}, errors.New("endpoint or selected branch was not found :( ") 572 } 573 574 return dtoResponse, nil 575 }