Skip to content

Commit

Permalink
feat: Add Bitbucket-Server Prebuilds Webhook (#1195)
Browse files Browse the repository at this point in the history
Signed-off-by: Ansuman Sahoo <[email protected]>
  • Loading branch information
unsuman authored Nov 8, 2024
1 parent 67f9ff6 commit 9bef1bc
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 52 deletions.
4 changes: 4 additions & 0 deletions cmd/daytona/config/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ func GetPrebuildScopesFromGitProviderId(providerId string) string {
return "admin:repo_hook"
case "bitbucket":
return "webhooks"
case "bitbucket-server":
return "REPO_ADMIN"
case "azure-devops":
return "Work (Read, Write & Manage); Build (Read & Execute)"
default:
Expand All @@ -166,6 +168,8 @@ func GetWebhookEventHeaderKeyFromGitProvider(providerId string) string {
return "X-Gitlab-Event"
case "bitbucket":
return "X-Event-Key"
case "bitbucket-server":
return "X-Event-Key"
case "gitea":
return "X-Gitea-Event"
case "azure-devops":
Expand Down
221 changes: 175 additions & 46 deletions pkg/gitprovider/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (
"net/http"
"net/url"

"github.com/daytonaio/daytona/internal/util"
bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
bitbucketWebhook "github.com/go-playground/webhooks/v6/bitbucket-server"
"github.com/mitchellh/mapstructure"
)

Expand Down Expand Up @@ -66,7 +68,6 @@ func (g *BitbucketServerGitProvider) GetNamespaces(options ListOptions) ([]*GitN
var namespaces []*GitNamespace

projectsRaw, err := client.DefaultApi.GetProjects(map[string]any{
"start": options.Page,
"limit": options.PerPage,
})
if err != nil {
Expand Down Expand Up @@ -98,57 +99,66 @@ func (g *BitbucketServerGitProvider) GetRepositories(namespace string, options L
}

var response []*GitRepository
var repoList *bitbucketv1.APIResponse
start := 0
for {
var repoList *bitbucketv1.APIResponse
opts := map[string]interface{}{
"start": start,
"limit": options.PerPage,
}
if namespace == personalNamespaceId {
repoList, err = client.DefaultApi.GetRepositories_19(opts)
} else {
repoList, err = client.DefaultApi.GetRepositoriesWithOptions(namespace, opts)
}

opts := map[string]interface{}{
"start": options.Page,
"limit": options.PerPage,
}
if namespace == personalNamespaceId {
repoList, err = client.DefaultApi.GetRepositories_19(opts)
} else {
repoList, err = client.DefaultApi.GetRepositoriesWithOptions(namespace, opts)
}
if err != nil {
return nil, err
}

if err != nil {
return nil, err
}
pageRepos, err := bitbucketv1.GetRepositoriesResponse(repoList)
if err != nil {
return nil, err
}

pageRepos, err := bitbucketv1.GetRepositoriesResponse(repoList)
if err != nil {
return nil, err
}
for _, repo := range pageRepos {
var repoUrl string
for _, link := range repo.Links.Clone {
if link.Name == "https" || link.Name == "http" {
repoUrl = link.Href
break
}
}

for _, repo := range pageRepos {
var repoUrl string
for _, link := range repo.Links.Clone {
if link.Name == "https" || link.Name == "http" {
repoUrl = link.Href
break
if len(repoUrl) == 0 && repo.Links != nil {
repoUrl = repo.Links.Self[0].Href
}
}

if len(repoUrl) == 0 && repo.Links != nil {
repoUrl = repo.Links.Self[0].Href
}
var ownerName string
if repo.Owner != nil {
ownerName = repo.Owner.Name
}

baseURL, err := url.Parse(g.baseApiUrl)
if err != nil {
return nil, g.FormatError(repoList.StatusCode, repoList.Message)
}

var ownerName string
if repo.Owner != nil {
ownerName = repo.Owner.Name
response = append(response, &GitRepository{
Id: repo.Slug,
Name: repo.Name,
Url: repoUrl,
Source: baseURL.Host,
Owner: ownerName,
})
}

baseURL, err := url.Parse(g.baseApiUrl)
if err != nil {
return nil, g.FormatError(repoList.StatusCode, repoList.Message)
hasNextPage, nextPageStart := bitbucketv1.HasNextPage(repoList)
if !hasNextPage {
break
}

response = append(response, &GitRepository{
Id: repo.Slug,
Name: repo.Name,
Url: repoUrl,
Source: baseURL.Host,
Owner: ownerName,
})
start = nextPageStart
}

return response, nil
Expand All @@ -162,10 +172,7 @@ func (g *BitbucketServerGitProvider) GetRepoBranches(repositoryId string, namesp

var response []*GitBranch

branches, err := client.DefaultApi.GetBranches(namespaceId, repositoryId, map[string]interface{}{
"start": options.Page,
"limit": options.PerPage,
})
branches, err := client.DefaultApi.GetBranches(repositoryId, namespaceId, nil)
if err != nil {
return nil, g.FormatError(branches.StatusCode, branches.Message)
}
Expand Down Expand Up @@ -447,7 +454,7 @@ func (g *BitbucketServerGitProvider) ParseStaticGitContext(repoUrl string) (*Sta

staticContext.Id = projectKey
staticContext.Name = repoName
staticContext.Owner = projectKey
staticContext.Owner = repoName
// For '.git' or repo clone over https format, refer to https://community.atlassian.com/t5/Bitbucket-questions/Project-key-in-repositories-URL/qaq-p/578207
// and https://community.atlassian.com/t5/Bitbucket-questions/remote-url-in-Bitbucket-server-what-does-scm-represent-is-it/qaq-p/2060987
staticContext.Url = fmt.Sprintf("%s/scm/%s/%s.git", baseUrl, projectKey, repoName)
Expand Down Expand Up @@ -521,3 +528,125 @@ func (b *BitbucketServerGitProvider) FormatError(statusCode int, message string)

return fmt.Errorf("status code: %d err: Request failed with %s", statusCode, message)
}

func (g *BitbucketServerGitProvider) RegisterPrebuildWebhook(repo *GitRepository, endpointUrl string) (string, error) {
client, err := g.getApiClient()
if err != nil {
return "", err
}

webhook := map[string]interface{}{
"url": endpointUrl,
"events": []string{"repo:refs_changed"},
"active": true,
}

contentType := []string{"application/json"}
hook, err := client.DefaultApi.CreateWebhook(repo.Id, repo.Owner, webhook, contentType)
if err != nil {
return "", g.FormatError(hook.StatusCode, hook.Message)
}

idVal := hook.Values["id"].(float64)
return fmt.Sprintf("%d", int(idVal)), nil
}

func (g *BitbucketServerGitProvider) GetPrebuildWebhook(repo *GitRepository, endpointUrl string) (*string, error) {
client, err := g.getApiClient()
if err != nil {
return nil, err
}

hooks, err := client.DefaultApi.FindWebhooks(repo.Id, repo.Owner, nil)

if err != nil {
return nil, g.FormatError(hooks.StatusCode, hooks.Message)
}

if hooks.Values == nil {
return nil, nil
}

for _, hook := range hooks.Values["values"].([]interface{}) {
idVal := hook.(map[string]interface{})["id"].(float64)
id := fmt.Sprintf("%d", int(idVal))
url := hook.(map[string]interface{})["url"].(string)
if url == endpointUrl {
return util.Pointer(id), nil
}
}

return nil, nil
}

func (g *BitbucketServerGitProvider) UnregisterPrebuildWebhook(repo *GitRepository, id string) error {
client, err := g.getApiClient()
if err != nil {
return err
}

hookId, err := strconv.Atoi(id)
if err != nil {
return errors.New("unable to convert webhook id to int")
}

resp, err := client.DefaultApi.DeleteWebhook(repo.Id, repo.Owner, int32(hookId))
if err != nil {
return g.FormatError(resp.StatusCode, resp.Message)
}

return nil
}

func (g *BitbucketServerGitProvider) GetCommitsRange(repo *GitRepository, initialSha string, currentSha string) (int, error) {
client, err := g.getApiClient()
if err != nil {
return 0, err
}

opts := map[string]interface{}{
"since": initialSha,
"until": currentSha,
}

commits, err := client.DefaultApi.GetCommits(repo.Id, repo.Owner, opts)
if err != nil {
return 0, g.FormatError(commits.StatusCode, commits.Message)
}

size := commits.Values["size"].(float64)
return int(size), nil
}

func (g *BitbucketServerGitProvider) ParseEventData(request *http.Request) (*GitEventData, error) {
if request.Header.Get("X-Event-Key") != "repo:refs_changed" {
return nil, errors.New("invalid event key")
}
hook, err := bitbucketWebhook.New()
if err != nil {
return nil, err
}

event, err := hook.Parse(request, bitbucketWebhook.RepositoryReferenceChangedEvent)
if err != nil {
return nil, errors.New("could not parse event")
}

pushEvent, ok := event.(bitbucketWebhook.RepositoryReferenceChangedPayload)
if !ok {
return nil, errors.New("could not parse push event")
}
owner := pushEvent.Actor.DisplayName
gitEventData := &GitEventData{
Url: fmt.Sprintf("%s/scm/%s/%s.git", strings.TrimSuffix(g.baseApiUrl, "/rest"), strings.ToLower(pushEvent.Repository.Project.Key), pushEvent.Repository.Slug),
Branch: strings.TrimPrefix(pushEvent.Changes[0].ReferenceID, "refs/heads/"),
Sha: pushEvent.Changes[0].ToHash,
Owner: owner,
}

for _, change := range pushEvent.Changes {
gitEventData.AffectedFiles = append(gitEventData.AffectedFiles, change.ToHash)
}

return gitEventData, nil
}
12 changes: 6 additions & 6 deletions pkg/gitprovider/bitbucketserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (b *BitbucketServerGitProviderTestSuite) TestParseStaticGitContext_PR() {
prContext := &StaticGitContext{
Id: "PROJECT_KEY",
Name: "REPO_NAME",
Owner: "PROJECT_KEY",
Owner: "REPO_NAME",
Url: "https://bitbucket.example.com/scm/PROJECT_KEY/REPO_NAME.git",
Source: "bitbucket.example.com",
Branch: nil,
Expand All @@ -62,7 +62,7 @@ func (b *BitbucketServerGitProviderTestSuite) TestParseStaticGitContext_Blob() {
blobContext := &StaticGitContext{
Id: "PROJECT_KEY",
Name: "REPO_NAME",
Owner: "PROJECT_KEY",
Owner: "REPO_NAME",
Url: "https://bitbucket.example.com/scm/PROJECT_KEY/REPO_NAME.git",
Source: "bitbucket.example.com",
Branch: nil,
Expand All @@ -84,7 +84,7 @@ func (b *BitbucketServerGitProviderTestSuite) TestParseStaticGitContext_Branch()
branchContext := &StaticGitContext{
Id: "PROJECT_KEY",
Name: "REPO_NAME",
Owner: "PROJECT_KEY",
Owner: "REPO_NAME",
Url: "https://bitbucket.example.com/scm/PROJECT_KEY/REPO_NAME.git",
Source: "bitbucket.example.com",
Branch: util.Pointer("main"),
Expand All @@ -106,7 +106,7 @@ func (b *BitbucketServerGitProviderTestSuite) TestParseStaticGitContext_Commits(
commitsContext := &StaticGitContext{
Id: "PROJECT_KEY",
Name: "REPO_NAME",
Owner: "PROJECT_KEY",
Owner: "REPO_NAME",
Url: "https://bitbucket.example.com/scm/PROJECT_KEY/REPO_NAME.git",
Source: "bitbucket.example.com",
Branch: util.Pointer("COMMIT_SHA"),
Expand All @@ -128,7 +128,7 @@ func (b *BitbucketServerGitProviderTestSuite) TestParseStaticGitContext_Commit()
commitContext := &StaticGitContext{
Id: "PROJECT_KEY",
Name: "REPO_NAME",
Owner: "PROJECT_KEY",
Owner: "REPO_NAME",
Url: "https://bitbucket.example.com/scm/PROJECT_KEY/REPO_NAME.git",
Source: "bitbucket.example.com",
Branch: util.Pointer("COMMIT_SHA"),
Expand All @@ -151,7 +151,7 @@ func (b *BitbucketServerGitProviderTestSuite) TestParseStaticGitContext_Repo_Url
commitContext := &StaticGitContext{
Id: "PROJECT_KEY",
Name: "REPO_NAME",
Owner: "PROJECT_KEY",
Owner: "REPO_NAME",
Url: "https://bitbucket.example.com/scm/PROJECT_KEY/REPO_NAME.git",
Source: "bitbucket.example.com",
Branch: util.Pointer("COMMIT_SHA"),
Expand Down

0 comments on commit 9bef1bc

Please sign in to comment.