Bitbucket(自行托管)
在本示例中,您将在 Bitbucket 服务器和 Port 之间创建 webhook 集成,该集成将有助于将 Bitbucket 项目、版本库和拉取请求实体导入 Port。
Port 配置
创建以下蓝图定义:
Bitbucket project blueprint
{
"identifier": "bitbucketProject",
"description": "A software catalog to represent Bitbucket project",
"title": "Bitbucket Project",
"icon": "BitBucket",
"schema": {
"properties": {
"public": {
"icon": "DefaultProperty",
"title": "Public",
"type": "boolean"
},
"description": {
"title": "Description",
"type": "string",
"icon": "DefaultProperty"
},
"type": {
"icon": "DefaultProperty",
"title": "Type",
"type": "string"
},
"link": {
"title": "Link",
"icon": "DefaultProperty",
"type": "string"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {}
}
Bitbucket repository blueprint
{
"identifier": "bitbucketRepository",
"description": "A software catalog to represent Bitbucket repositories",
"title": "Bitbucket Repository",
"icon": "BitBucket",
"schema": {
"properties": {
"forkable": {
"icon": "DefaultProperty",
"title": "Is Forkable",
"type": "boolean"
},
"description": {
"title": "Description",
"type": "string",
"icon": "DefaultProperty"
},
"public": {
"icon": "DefaultProperty",
"title": "Is Public",
"type": "boolean"
},
"state": {
"icon": "DefaultProperty",
"title": "State",
"type": "string"
},
"link": {
"title": "Link",
"icon": "DefaultProperty",
"type": "string"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {
"project": {
"title": "Project",
"target": "bitbucketProject",
"required": false,
"many": false
}
}
}
Bitbucket pull request blueprint
{
"identifier": "bitbucketPullrequest",
"description": "A software catalog to represent Bitbucket pull requests",
"title": "Bitbucket Pull Request",
"icon": "BitBucket",
"schema": {
"properties": {
"created_on": {
"title": "Created On",
"type": "string",
"format": "date-time",
"icon": "DefaultProperty"
},
"updated_on": {
"title": "Updated On",
"type": "string",
"format": "date-time",
"icon": "DefaultProperty"
},
"description": {
"title": "Description",
"type": "string",
"icon": "DefaultProperty"
},
"state": {
"icon": "DefaultProperty",
"title": "State",
"type": "string",
"enum": [
"OPEN",
"MERGED",
"DECLINED",
"SUPERSEDED"
],
"enumColors": {
"OPEN": "yellow",
"MERGED": "green",
"DECLINED": "red",
"SUPERSEDED": "purple"
}
},
"owner": {
"title": "Owner",
"type": "string",
"icon": "DefaultProperty"
},
"link": {
"title": "Link",
"icon": "DefaultProperty",
"type": "string"
},
"destination": {
"title": "Destination Branch",
"type": "string",
"icon": "DefaultProperty"
},
"source": {
"title": "Source Branch",
"type": "string",
"icon": "DefaultProperty"
},
"reviewers": {
"items": {
"type": "string"
},
"title": "Reviewers",
"type": "array",
"icon": "DefaultProperty"
},
"participants": {
"items": {
"type": "string"
},
"title": "Participants",
"type": "array",
"icon": "DefaultProperty"
},
"merge_commit": {
"title": "Merge Commit",
"type": "string",
"icon": "DefaultProperty"
}
},
"required": []
},
"mirrorProperties": {},
"calculationProperties": {},
"relations": {
"repository": {
"title": "Repository",
"target": "bitbucketRepository",
"required": false,
"many": false
}
}
}
蓝图属性 您可以根据 Bitbucket 账户中需要跟踪的内容,修改蓝图中的属性。
创建以下 webhook 配置using Port's UI
Bitbucket webhook configuration
- 基本信息 选项卡 - 填写以下详细信息:
1.title: "Bitbucket 服务器映射器";
2.标识符 :
bitbucket_server_mapper
; 3.Description :将 Bitbucket 项目、版本库和拉取请求映射到 Port
的 webhook 配置; 4.图标 :BitBucket
; - 集成配置选项卡--填写以下JQ映射:
::注意 注意并复制此选项卡中提供的 Webhook URL ::: 3.点击页面底部的保存。
[
{
"blueprint": "bitbucketProject",
"filter": ".body.eventKey == \"project:modified\"",
"entity": {
"identifier": ".body.new.key | tostring",
"title": ".body.new.name",
"properties": {
"public": ".body.new.public",
"type": ".body.new.type",
"description": ".body.new.description",
"link": ".body.new.links.self[0].href"
}
}
},
{
"blueprint": "bitbucketRepository",
"filter": ".body.eventKey == \"repo:modified\"",
"entity": {
"identifier": ".body.new.slug",
"title": ".body.new.name",
"properties": {
"description": ".body.new.description",
"state": ".body.new.state",
"forkable": ".body.new.forkable",
"public": ".body.new.public",
"link": ".body.new.links.self[0].href"
},
"relations": {
"project": ".body.new.project.key"
}
}
},
{
"blueprint": "bitbucketPullrequest",
"filter": ".body.eventKey | startswith(\"pr:\")",
"entity": {
"identifier": ".body.pullRequest.id | tostring",
"title": ".body.pullRequest.title",
"properties": {
"created_on": ".body.pullRequest.createdDate | (tonumber / 1000 | strftime(\"%Y-%m-%dT%H:%M:%SZ\"))",
"updated_on": ".body.pullRequest.updatedDate | (tonumber / 1000 | strftime(\"%Y-%m-%dT%H:%M:%SZ\"))",
"merge_commit": ".body.pullRequest.fromRef.latestCommit",
"state": ".body.pullRequest.state",
"owner": ".body.pullRequest.author.user.displayName",
"link": ".body.pullRequest.links.self[0].href",
"destination": ".body.pullRequest.toRef.displayId",
"source": ".body.pullRequest.fromRef.displayId",
"participants": "[.body.pullRequest.participants[].user.displayName]",
"reviewers": "[.body.pullRequest.reviewers[].user.displayName]"
},
"relations": {
"repository": ".body.pullRequest.toRef.repository.slug"
}
}
}
]
在 Bitbucket 中创建 webhook
- 从 Bitbucket 账户打开要添加 webhook 的项目;
- 点击项目设置或左侧边栏的齿轮图标;
- 在工作流程部分,选择左侧边栏上的Webhooks;
- 单击添加网络钩子按钮,为版本库创建网络钩子;
- 输入以下详细信息:
- title` - 使用一个有意义的名称,如 Port Webhook;
URL
- 输入在 Port 中创建 webhook 配置后收到的 webhookURL
的值;Secret
- 输入您在 Port 中配置 webhook 时提供的secret值;- 触发器
1.在 项目下选择
modified
; 2.在 Repository 下选择modified
; 3.在拉取请求下,根据用例选择任意事件。 6.单击保存保存 webhook;
请访问this documentation 了解有关 Bitbucket 中 webhook 事件有效载荷的更多信息。
完成!您在 Bitbucket 中的项目、版本库或拉取请求发生的任何更改都会触发 webhook 事件,并发送到 Port 提供的 webhook URL。 Port 会根据映射解析事件,并相应地更新目录实体。
让我们来测试一下
本节包括一个当拉取请求被合并时从 Bitbucket 发送的 webhook 事件示例。 此外,本节还包括根据上一节提供的 webhook 配置从该事件创建的实体。
有效载荷
以下是 Bitbucket 拉取请求合并时发送到 webhook URL 的有效载荷结构示例:
Webhook event payload
{
"body": {
"eventKey": "pr:merged",
"date": "2023-11-16T11:03:42+0000",
"actor": {
"name": "admin",
"emailAddress": "[email protected]",
"active": true,
"displayName": "Test User",
"id": 2,
"slug": "admin",
"type": "NORMAL",
"links": {
"self": [
{
"href": "http://myhost:7990/users/admin"
}
]
}
},
"pullRequest": {
"id": 2,
"version": 2,
"title": "lint code",
"description": "here is the description",
"state": "MERGED",
"open": false,
"closed": true,
"createdDate": 1700132280533,
"updatedDate": 1700132622026,
"closedDate": 1700132622026,
"fromRef": {
"id": "refs/heads/dev",
"displayId": "dev",
"latestCommit": "9e08604e14fa72265d65696608725c2b8f7850f2",
"type": "BRANCH",
"repository": {
"slug": "data-analyses",
"id": 1,
"name": "data analyses",
"description": "This is for my repository and all the blah blah blah",
"hierarchyId": "24cfae4b0dd7bade7edc",
"scmId": "git",
"state": "AVAILABLE",
"statusMessage": "Available",
"forkable": true,
"project": {
"key": "MOPP",
"id": 1,
"name": "My On Prem Project",
"description": "On premise test project is sent to us for us",
"public": false,
"type": "NORMAL",
"links": {
"self": [
{
"href": "http://myhost:7990/projects/MOPP"
}
]
}
},
"public": false,
"archived": false,
"links": {
"clone": [
{
"href": "ssh://git@myhost:7999/mopp/data-analyses.git",
"name": "ssh"
},
{
"href": "http://myhost:7990/scm/mopp/data-analyses.git",
"name": "http"
}
],
"self": [
{
"href": "http://myhost:7990/projects/MOPP/repos/data-analyses/browse"
}
]
}
}
},
"toRef": {
"id": "refs/heads/main",
"displayId": "main",
"latestCommit": "e461aae894b6dc951f405dca027a3f5567ea6bee",
"type": "BRANCH",
"repository": {
"slug": "data-analyses",
"id": 1,
"name": "data analyses",
"description": "This is for my repository and all the blah blah blah",
"hierarchyId": "24cfae4b0dd7bade7edc",
"scmId": "git",
"state": "AVAILABLE",
"statusMessage": "Available",
"forkable": true,
"project": {
"key": "MOPP",
"id": 1,
"name": "My On Prem Project",
"description": "On premise test project is sent to us for us",
"public": false,
"type": "NORMAL",
"links": {
"self": [
{
"href": "http://myhost:7990/projects/MOPP"
}
]
}
},
"public": false,
"archived": false,
"links": {
"clone": [
{
"href": "ssh://git@myhost:7999/mopp/data-analyses.git",
"name": "ssh"
},
{
"href": "http://myhost:7990/scm/mopp/data-analyses.git",
"name": "http"
}
],
"self": [
{
"href": "http://myhost:7990/projects/MOPP/repos/data-analyses/browse"
}
]
}
}
},
"locked": false,
"author": {
"user": {
"name": "admin",
"emailAddress": "[email protected]",
"active": true,
"displayName": "Test User",
"id": 2,
"slug": "admin",
"type": "NORMAL",
"links": {
"self": [
{
"href": "http://myhost:7990/users/admin"
}
]
}
},
"role": "AUTHOR",
"approved": false,
"status": "UNAPPROVED"
},
"reviewers": [],
"participants": [],
"properties": {
"mergeCommit": {
"displayId": "1cbccf99220",
"id": "1cbccf99220b23f89624c7c604f630663a1aaf8e"
}
},
"links": {
"self": [
{
"href": "http://myhost:7990/projects/MOPP/repos/data-analyses/pull-requests/2"
}
]
}
}
},
"headers": {
"X-Forwarded-For": "10.0.148.57",
"X-Forwarded-Proto": "https",
"X-Forwarded-Port": "443",
"Host": "ingest.getport.io",
"X-Amzn-Trace-Id": "Self=1-6555f719-267a0fce1e7a4d8815de94f7;Root=1-6555f719-1906872f41621b17250bb83a",
"Content-Length": "2784",
"User-Agent": "Atlassian HttpClient 3.0.4 / Bitbucket-8.15.1 (8015001) / Default",
"Content-Type": "application/json; charset=UTF-8",
"accept": "*/*",
"X-Event-Key": "pr:merged",
"X-Hub-Signature": "sha256=bf366faf8d8c41a4af21d25d922b87c3d1d127b5685238b099d2f311ad46e978",
"X-Request-Id": "d5fa6a16-bb6c-40d6-9c50-bc4363e79632",
"via": "HTTP/1.1 AmazonAPIGateway",
"forwarded": "for=154.160.30.235;host=ingest.getport.io;proto=https"
},
"queryParams": {}
}
映射结果
{
"identifier":"2",
"title":"lint code",
"blueprint":"bitbucketPullrequest",
"properties":{
"created_on":"2023-11-16T10:58:00Z",
"updated_on":"2023-11-16T11:03:42Z",
"merge_commit":"9e08604e14fa72265d65696608725c2b8f7850f2",
"state":"MERGED",
"owner":"Test User",
"link":"http://myhost:7990/projects/MOPP/repos/data-analyses/pull-requests/2",
"destination":"main",
"source":"dev",
"participants":[],
"reviewers":[]
},
"relations":{
"repository":"data-analyses"
},
"filter":true
}
导入 Bitbucket 历史问题
在本示例中,您将使用 Provider 提供的 Python 脚本从 Bitbucket 服务器 API 获取数据并将其引用到 Port。
先决条件
本示例使用的是上一节中的blueprint and webhook 定义。
此外,请 Provider 下列环境变量:
PORT_CLIENT_ID
- 您的 Port 客户端 IDPORT_CLIENT_SECRET
- 您的 Port 客户端secretBITBUCKET_HOST
- Bitbucket 服务器主机,例如http://localhost:7990
.BITBUCKET_USERNAME
- 访问 Bitbucket 资源时被用于的 Bitbucket 用户名BITBUCKET_PASSWORD
- Bitbucket 账号密码
使用以下命令查找您的 Port 凭据guide
使用以下 Python 脚本将历史 Bitbucket 项目、源和拉取请求引用到 Port 中:
Bitbucket Python script
## Import the needed libraries
import requests
from requests.auth import HTTPBasicAuth
from decouple import config
from loguru import logger
from typing import Any
import time
from datetime import datetime
# Get environment variables using the config object or os.environ["KEY"]
PORT_CLIENT_ID = config("PORT_CLIENT_ID")
PORT_CLIENT_SECRET = config("PORT_CLIENT_SECRET")
BITBUCKET_USERNAME = config("BITBUCKET_USERNAME")
BITBUCKET_PASSWORD = config("BITBUCKET_PASSWORD")
BITBUCKET_API_URL = config("BITBUCKET_HOST")
PORT_API_URL = "https://api.getport.io/v1"
## According to https://support.atlassian.com/bitbucket-cloud/docs/api-request-limits/
RATE_LIMIT = 1000 # Maximum number of requests allowed per hour
RATE_PERIOD = 3600 # Rate limit reset period in seconds (1 hour)
# Initialize rate limiting variables
request_count = 0
rate_limit_start = time.time()
## Get Port Access Token
credentials = {'clientId': PORT_CLIENT_ID, 'clientSecret': PORT_CLIENT_SECRET}
token_response = requests.post(f'{PORT_API_URL}/auth/access_token', json=credentials)
access_token = token_response.json()['accessToken']
# You can now use the value in access_token when making further requests
port_headers = {
'Authorization': f'Bearer {access_token}'
}
## Bitbucket user password https://developer.atlassian.com/server/bitbucket/how-tos/example-basic-authentication/
bitbucket_auth = HTTPBasicAuth(username=BITBUCKET_USERNAME, password=BITBUCKET_PASSWORD)
def add_entity_to_port(blueprint_id, entity_object):
response = requests.post(f'{PORT_API_URL}/blueprints/{blueprint_id}/entities?upsert=true&merge=true', json=entity_object, headers=port_headers)
logger.info(response.json())
def get_paginated_resource(path: str, params: dict[str, Any] = None, page_size: int = 25):
logger.info(f"Requesting data for {path}")
global request_count, rate_limit_start
# Check if we've exceeded the rate limit, and if so, wait until the reset period is over
if request_count >= RATE_LIMIT:
elapsed_time = time.time() - rate_limit_start
if elapsed_time < RATE_PERIOD:
sleep_time = RATE_PERIOD - elapsed_time
time.sleep(sleep_time)
# Reset the rate limiting variables
request_count = 0
rate_limit_start = time.time()
url = f"{BITBUCKET_API_URL}/rest/api/1.0/{path}"
params = params or {}
params["limit"] = page_size
next_page_start = None
while True:
try:
if next_page_start:
params["start"] = next_page_start
response = requests.get(url=url, auth=bitbucket_auth, params=params)
response.raise_for_status()
page_json = response.json()
request_count += 1
batch_data = page_json["values"]
yield batch_data
# Check for next page start in response
next_page_start = page_json.get("nextPageStart")
# Break the loop if there is no more data
if not next_page_start:
break
except requests.exceptions.HTTPError as e:
logger.error(f"HTTP error with code {e.response.status_code}, content: {e.response.text}")
raise
logger.info(f"Successfully fetched paginated data for {path}")
def convert_to_datetime(timestamp: int):
converted_datetime = datetime.utcfromtimestamp(timestamp / 1000.0)
return converted_datetime.strftime('%Y-%m-%dT%H:%M:%SZ')
def process_project_entities(projects_data: list[dict[str, Any]]):
blueprint_id = "bitbucketProject"
for project in projects_data:
entity = {
"identifier": project["key"],
"title": project["name"],
"properties": {
"description": project.get("description"),
"public": project["public"],
"type": project["type"],
"link": project["links"]["self"][0]["href"]
},
"relations": {}
}
add_entity_to_port(blueprint_id=blueprint_id, entity_object=entity)
def process_repository_entities(repository_data: list[dict[str, Any]]):
blueprint_id = "bitbucketRepository"
for repo in repository_data:
entity = {
"identifier": repo["slug"],
"title": repo["name"],
"properties": {
"description": repo.get("description"),
"state": repo["state"],
"forkable": repo["forkable"],
"public": repo["public"],
"link": repo["links"]["self"][0]["href"]
},
"relations": {
"project": repo["project"]["key"]
}
}
add_entity_to_port(blueprint_id=blueprint_id, entity_object=entity)
def process_pullrequest_entities(pullrequest_data: list[dict[str, Any]]):
blueprint_id = "bitbucketPullrequest"
for pr in pullrequest_data:
entity = {
"identifier": str(pr["id"]),
"title": pr["title"],
"properties": {
"created_on": convert_to_datetime(pr["createdDate"]),
"updated_on": convert_to_datetime(pr["updatedDate"]),
"merge_commit": pr["fromRef"]["latestCommit"],
"description": pr.get("description"),
"state": pr["state"],
"owner": pr["author"]["user"]["displayName"],
"link": pr["links"]["self"][0]["href"],
"destination": pr["toRef"]["displayId"],
"participants": [user["user"]["displayName"] for user in pr.get("participants", [])],
"reviewers": [user["user"]["displayName"] for user in pr.get("reviewers", [])],
"source": pr["fromRef"]["displayId"]
},
"relations": {
"repository": pr["toRef"]["repository"]["slug"]
}
}
add_entity_to_port(blueprint_id=blueprint_id, entity_object=entity)
def get_repositories(project: dict[str, Any]):
repositories_path = f"projects/{project['key']}/repos"
for repositories_batch in get_paginated_resource(path=repositories_path):
logger.info(f"received repositories batch with size {len(repositories_batch)} from project: {project['key']}")
process_repository_entities(repository_data=repositories_batch)
get_repository_pull_requests(repository_batch=repositories_batch)
def get_repository_pull_requests(repository_batch: list[dict[str, Any]]):
pr_params = {"state": "ALL"} ## Fetch all pull requests
for repository in repository_batch:
pull_requests_path = f"projects/{repository['project']['key']}/repos/{repository['slug']}/pull-requests"
for pull_requests_batch in get_paginated_resource(path=pull_requests_path, params=pr_params):
logger.info(f"received pull requests batch with size {len(pull_requests_batch)} from repo: {repository['slug']}")
process_pullrequest_entities(pullrequest_data=pull_requests_batch)
if __name__ == "__main__":
project_path = "projects"
for projects_batch in get_paginated_resource(path=project_path):
logger.info(f"received projects batch with size {len(projects_batch)}")
process_project_entities(projects_data=projects_batch)
for project in projects_batch:
get_repositories(project=project)
完成!现在您可以将历史项目、版本库和拉取请求从 Bitbucket 服务器导入 Port。 Port 将根据映射解析对象,并相应地更新目录实体。