woodpecker/server/forge/bitbucketserver/internal/client.go
2023-04-07 18:09:17 +02:00

475 lines
12 KiB
Go

// Copyright 2022 Woodpecker Authors
// Copyright 2018 Drone.IO Inc.
//
// 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
//
// http://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 internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/mrjones/oauth"
"github.com/rs/zerolog/log"
"github.com/woodpecker-ci/woodpecker/server/model"
)
const (
currentUserID = "%s/plugins/servlet/applinks/whoami"
pathUser = "%s/rest/api/1.0/users/%s"
pathRepo = "%s/rest/api/1.0/projects/%s/repos/%s"
pathRepos = "%s/rest/api/1.0/repos?start=%s&limit=%s"
pathHook = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s"
pathSource = "%s/projects/%s/repos/%s/browse/%s?at=%s&raw"
hookName = "com.atlassian.stash.plugin.stash-web-post-receive-hooks-plugin:postReceiveHook"
pathHookDetails = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s"
pathHookEnabled = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/enabled"
pathHookSettings = "%s/rest/api/1.0/projects/%s/repos/%s/settings/hooks/%s/settings"
pathStatus = "%s/rest/build-status/1.0/commits/%s"
pathBranches = "%s/rest/api/1.0/projects/%s/repos/%s/branches"
)
type Client struct {
client *http.Client
base string
accessToken string
ctx context.Context
}
func NewClientWithToken(ctx context.Context, url string, consumer *oauth.Consumer, AccessToken string) *Client {
var token oauth.AccessToken
token.Token = AccessToken
client, err := consumer.MakeHttpClient(&token)
if err != nil {
log.Err(err).Msg("")
}
return &Client{
client: client,
base: url,
accessToken: AccessToken,
ctx: ctx,
}
}
func (c *Client) FindCurrentUser() (*User, error) {
CurrentUserIDResponse, err := c.doGet(fmt.Sprintf(currentUserID, c.base))
if CurrentUserIDResponse != nil {
defer CurrentUserIDResponse.Body.Close()
}
if err != nil {
return nil, err
}
bits, err := io.ReadAll(CurrentUserIDResponse.Body)
if err != nil {
return nil, err
}
login := string(bits)
CurrentUserResponse, err := c.doGet(fmt.Sprintf(pathUser, c.base, login))
if CurrentUserResponse != nil {
defer CurrentUserResponse.Body.Close()
}
if err != nil {
return nil, err
}
contents, err := io.ReadAll(CurrentUserResponse.Body)
if err != nil {
return nil, err
}
var user User
err = json.Unmarshal(contents, &user)
if err != nil {
return nil, err
}
return &user, nil
}
func (c *Client) FindRepo(owner, name string) (*Repo, error) {
urlString := fmt.Sprintf(pathRepo, c.base, owner, name)
response, err := c.doGet(urlString)
if response != nil {
defer response.Body.Close()
}
if err != nil {
log.Err(err).Msg("")
}
contents, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
repo := Repo{}
err = json.Unmarshal(contents, &repo)
if err != nil {
return nil, err
}
return &repo, nil
}
func (c *Client) FindRepos() ([]*Repo, error) {
return c.paginatedRepos(0)
}
func (c *Client) FindRepoPerms(owner, repo string) (*model.Perm, error) {
perms := new(model.Perm)
// If you don't have access return none right away
_, err := c.FindRepo(owner, repo)
if err != nil {
return perms, err
}
// Must have admin to be able to list hooks. If have access the enable perms
resp, err := c.doGet(fmt.Sprintf(pathHook, c.base, owner, repo, hookName))
if resp != nil {
defer resp.Body.Close()
}
if err == nil {
perms.Push = true
perms.Admin = true
}
perms.Pull = true
return perms, nil
}
func (c *Client) FindFileForRepo(owner, repo, fileName, ref string) ([]byte, error) {
response, err := c.doGet(fmt.Sprintf(pathSource, c.base, owner, repo, fileName, ref))
if response != nil {
defer response.Body.Close()
}
if err != nil {
log.Err(err).Msg("")
}
if response.StatusCode == 404 {
return nil, nil
}
responseBytes, err := io.ReadAll(response.Body)
if err != nil {
log.Err(err).Msg("")
}
return responseBytes, nil
}
func (c *Client) CreateHook(owner, name, callBackLink string) error {
hookDetails, err := c.GetHookDetails(owner, name)
if err != nil {
return err
}
var hooks []string
if hookDetails.Enabled {
hookSettings, err := c.GetHooks(owner, name)
if err != nil {
return err
}
hooks = hookSettingsToArray(hookSettings)
}
if !stringInSlice(callBackLink, hooks) {
hooks = append(hooks, callBackLink)
}
putHookSettings := arrayToHookSettings(hooks)
hookBytes, err := json.Marshal(putHookSettings)
if err != nil {
return err
}
return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes)
}
func (c *Client) CreateStatus(revision string, status *PipelineStatus) error {
uri := fmt.Sprintf(pathStatus, c.base, revision)
return c.doPost(uri, status)
}
func (c *Client) DeleteHook(owner, name, link string) error {
hookSettings, err := c.GetHooks(owner, name)
if err != nil {
return err
}
putHooks := filter(hookSettingsToArray(hookSettings), func(item string) bool {
return !strings.Contains(item, link)
})
putHookSettings := arrayToHookSettings(putHooks)
hookBytes, err := json.Marshal(putHookSettings)
if err != nil {
return err
}
return c.doPut(fmt.Sprintf(pathHookEnabled, c.base, owner, name, hookName), hookBytes)
}
func (c *Client) GetHookDetails(owner, name string) (*HookPluginDetails, error) {
urlString := fmt.Sprintf(pathHookDetails, c.base, owner, name, hookName)
response, err := c.doGet(urlString)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
hookDetails := HookPluginDetails{}
err = json.NewDecoder(response.Body).Decode(&hookDetails)
return &hookDetails, err
}
func (c *Client) GetHooks(owner, name string) (*HookSettings, error) {
urlString := fmt.Sprintf(pathHookSettings, c.base, owner, name, hookName)
response, err := c.doGet(urlString)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
hookSettings := HookSettings{}
err = json.NewDecoder(response.Body).Decode(&hookSettings)
return &hookSettings, err
}
// TODO: make these as as general do with the action
// Helper function to help create get
func (c *Client) doGet(url string) (*http.Response, error) {
request, err := http.NewRequestWithContext(c.ctx, "GET", url, nil)
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/json")
return c.client.Do(request)
}
// Helper function to help create the hook
func (c *Client) doPut(url string, body []byte) error {
request, err := http.NewRequestWithContext(c.ctx, "PUT", url, bytes.NewBuffer(body))
if err != nil {
return err
}
request.Header.Add("Content-Type", "application/json")
response, err := c.client.Do(request)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return err
}
return nil
}
// Helper function to help create the hook
func (c *Client) doPost(url string, status *PipelineStatus) error {
// write it to the body of the request.
var buf io.ReadWriter
if status != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(status)
if err != nil {
return err
}
}
request, err := http.NewRequestWithContext(c.ctx, "POST", url, buf)
if err != nil {
return err
}
request.Header.Add("Content-Type", "application/json")
response, err := c.client.Do(request)
if response != nil {
defer response.Body.Close()
}
return err
}
// Helper function to get repos paginated
func (c *Client) paginatedRepos(start int) ([]*Repo, error) {
limit := 1000
requestURL := fmt.Sprintf(pathRepos, c.base, strconv.Itoa(start), strconv.Itoa(limit))
response, err := c.doGet(requestURL)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return nil, err
}
var repoResponse Repos
err = json.NewDecoder(response.Body).Decode(&repoResponse)
if err != nil {
return nil, err
}
if !repoResponse.IsLastPage {
reposList, err := c.paginatedRepos(start + limit)
if err != nil {
return nil, err
}
repoResponse.Values = append(repoResponse.Values, reposList...)
}
return repoResponse.Values, nil
}
func (c *Client) ListBranches(owner, name string) ([]*Branch, error) {
uri := fmt.Sprintf(pathBranches, c.base, owner, name)
response, err := c.doGet(uri)
if err != nil {
return nil, err
}
defer response.Body.Close()
out := new(BranchResp)
err = json.NewDecoder(response.Body).Decode(&out)
return out.Values, err
}
func filter(vs []string, f func(string) bool) []string {
var vsf []string
for _, v := range vs {
if f(v) {
vsf = append(vsf, v)
}
}
return vsf
}
// TODO: find a clean way of doing these next two methods- bitbucket server hooks only support 20 cb hooks
func arrayToHookSettings(hooks []string) HookSettings {
hookSettings := HookSettings{}
for loc, value := range hooks {
switch loc {
case 0:
hookSettings.HookURL0 = value
case 1:
hookSettings.HookURL1 = value
case 2:
hookSettings.HookURL2 = value
case 3:
hookSettings.HookURL3 = value
case 4:
hookSettings.HookURL4 = value
case 5:
hookSettings.HookURL5 = value
case 6:
hookSettings.HookURL6 = value
case 7:
hookSettings.HookURL7 = value
case 8:
hookSettings.HookURL8 = value
case 9:
hookSettings.HookURL9 = value
case 10:
hookSettings.HookURL10 = value
case 11:
hookSettings.HookURL11 = value
case 12:
hookSettings.HookURL12 = value
case 13:
hookSettings.HookURL13 = value
case 14:
hookSettings.HookURL14 = value
case 15:
hookSettings.HookURL15 = value
case 16:
hookSettings.HookURL16 = value
case 17:
hookSettings.HookURL17 = value
case 18:
hookSettings.HookURL18 = value
case 19:
hookSettings.HookURL19 = value
// Since there's only 19 hooks it will add to the latest if it doesn't exist :/
default:
hookSettings.HookURL19 = value
}
}
return hookSettings
}
func hookSettingsToArray(hookSettings *HookSettings) []string {
var hooks []string
if hookSettings.HookURL0 != "" {
hooks = append(hooks, hookSettings.HookURL0)
}
if hookSettings.HookURL1 != "" {
hooks = append(hooks, hookSettings.HookURL1)
}
if hookSettings.HookURL2 != "" {
hooks = append(hooks, hookSettings.HookURL2)
}
if hookSettings.HookURL3 != "" {
hooks = append(hooks, hookSettings.HookURL3)
}
if hookSettings.HookURL4 != "" {
hooks = append(hooks, hookSettings.HookURL4)
}
if hookSettings.HookURL5 != "" {
hooks = append(hooks, hookSettings.HookURL5)
}
if hookSettings.HookURL6 != "" {
hooks = append(hooks, hookSettings.HookURL6)
}
if hookSettings.HookURL7 != "" {
hooks = append(hooks, hookSettings.HookURL7)
}
if hookSettings.HookURL8 != "" {
hooks = append(hooks, hookSettings.HookURL8)
}
if hookSettings.HookURL9 != "" {
hooks = append(hooks, hookSettings.HookURL9)
}
if hookSettings.HookURL10 != "" {
hooks = append(hooks, hookSettings.HookURL10)
}
if hookSettings.HookURL11 != "" {
hooks = append(hooks, hookSettings.HookURL11)
}
if hookSettings.HookURL12 != "" {
hooks = append(hooks, hookSettings.HookURL12)
}
if hookSettings.HookURL13 != "" {
hooks = append(hooks, hookSettings.HookURL13)
}
if hookSettings.HookURL14 != "" {
hooks = append(hooks, hookSettings.HookURL14)
}
if hookSettings.HookURL15 != "" {
hooks = append(hooks, hookSettings.HookURL15)
}
if hookSettings.HookURL16 != "" {
hooks = append(hooks, hookSettings.HookURL16)
}
if hookSettings.HookURL17 != "" {
hooks = append(hooks, hookSettings.HookURL17)
}
if hookSettings.HookURL18 != "" {
hooks = append(hooks, hookSettings.HookURL18)
}
if hookSettings.HookURL19 != "" {
hooks = append(hooks, hookSettings.HookURL19)
}
return hooks
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}