WebHook broker that accepts notifications from multiple platforms and performs simple actions in response
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

main.py 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import hashlib
  2. import socket
  3. import jenkins
  4. import requests
  5. import os
  6. from bs4 import BeautifulSoup
  7. from flask import Flask, abort, request
  8. BASE_URL = os.environ["LAS_BASE_URL"]
  9. SECRET = os.environ["LAS_SECRET"]
  10. jenkins_server = jenkins.Jenkins(
  11. os.environ["LAS_JENKINS_URL"],
  12. username=os.environ["LAS_JENKINS_USER"],
  13. password=os.environ["LAS_JENKINS_PASSWORD"],
  14. )
  15. def get_hook_key(service, identifier):
  16. nonce = (service + SECRET + identifier).encode("ascii")
  17. return hashlib.sha256(nonce).hexdigest()
  18. def get_hook_url(service, identifier):
  19. return f"{BASE_URL}hooks/{service}/{identifier}/{get_hook_key(service, identifier)}"
  20. def get_jenkins_jobs():
  21. for job in jenkins_server.get_all_jobs():
  22. config = BeautifulSoup(
  23. jenkins_server.get_job_config(job["fullname"]), features="xml"
  24. )
  25. for git_config in config.find_all("scm", class_="hudson.plugins.git.GitSCM"):
  26. branch_spec = git_config.find("branches").find("name").text
  27. yield job["fullname"], branch_spec, git_config.find("url").text
  28. def gitea_request(method, api_path, **kwargs):
  29. if "params" not in kwargs:
  30. kwargs["params"] = {}
  31. kwargs["params"]["access_token"] = os.environ["LAS_GITEA_TOKEN"]
  32. return requests.request(
  33. method, f"{os.environ['LAS_GITEA_URL']}api/v1/{api_path}", **kwargs
  34. )
  35. def maybe_install_gitea_hook(project):
  36. hook_url = get_hook_url("gitea", project)
  37. path = f"repos/{project}/hooks"
  38. hooks = gitea_request("get", path).json()
  39. if hook_url not in [hook["config"]["url"] for hook in hooks]:
  40. body = {
  41. "active": True,
  42. "config": {"content_type": "json", "url": hook_url},
  43. "events": [
  44. "create",
  45. "delete",
  46. "fork",
  47. "push",
  48. "issues",
  49. "issue_comment",
  50. "pull_request",
  51. "repository",
  52. "release",
  53. ],
  54. "type": "gitea",
  55. }
  56. gitea_request("post", path, json=body).json()
  57. def get_gitea_repos():
  58. repos = gitea_request("get", f"user/repos").json()
  59. for repo in repos:
  60. maybe_install_gitea_hook(repo["full_name"])
  61. yield repo["full_name"], repo["ssh_url"], repo["clone_url"]
  62. def reportbot_announce(message):
  63. try:
  64. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
  65. host = os.environ["LAS_REPORTBOT_ADDRESS"].split(":")
  66. sock.connect((host[0], int(host[1])))
  67. sock.sendall(
  68. f"{os.environ['LAS_REPORTBOT_PREFIX']} {message}\n".encode("utf-8")
  69. )
  70. app.logger.info(f"Report bot response: {sock.recv(512)}")
  71. except Exception:
  72. app.logger.exception("Unable to send report bot message")
  73. repos = dict((name, [ssh, clone]) for name, ssh, clone in get_gitea_repos())
  74. jobs = list(get_jenkins_jobs())
  75. app = Flask(__name__)
  76. @app.route("/")
  77. def handle_index():
  78. return app.send_static_file("index.html")
  79. @app.route("/hooks/gitea/<path:repo>/<hash>", methods=["POST"])
  80. def handle_hook_gitea(repo, hash):
  81. app.logger.info(f"Received hook for repo {repo} with has {hash}")
  82. expected_hash = get_hook_key("gitea", repo)
  83. if hash != expected_hash:
  84. app.logger.info(f"Hash mismatch: expected {expected_hash}")
  85. abort(403)
  86. if repo not in repos:
  87. app.logger.info(f"Repository not found. Known repos: {repos.keys()}")
  88. abort(404)
  89. if request.headers.get("X-Gitea-Event") == "push":
  90. urls = repos[repo]
  91. for name, spec, url in jobs:
  92. if url in urls:
  93. # TODO: Check branches
  94. app.logger.info(f"Found matching job: {name} with URL {url}")
  95. jenkins_server.build_job(name)
  96. data = request.get_json()
  97. if not data["repository"]["private"]:
  98. repo = data["repository"]["full_name"]
  99. commits = len(data["commits"])
  100. compare = data["compare_url"]
  101. pusher = data["pusher"]["login"]
  102. reportbot_announce(
  103. f"\002[git]\002 {pusher} pushed {commits} commit{'s' if commits != 1 else ''} to {repo}: {compare}"
  104. )
  105. for commit in data["commits"][:3]:
  106. reportbot_announce(
  107. f"\002[git]\002 {commit['id'][:10]}: {commit['message'][:100]}"
  108. )
  109. return "", 204
  110. @app.route("/hooks/docker/registry/<hash>", methods=["GET", "POST"])
  111. def handle_docker_registry(hash):
  112. expected_hash = get_hook_key("docker", "registry")
  113. if hash != expected_hash:
  114. app.logger.info(f"Hash mismatch: expected {expected_hash}")
  115. abort(403)
  116. for event in request.get_json()["events"]:
  117. if (
  118. event["action"] == "push"
  119. and "vnd.docker.distribution.manifest" in event["target"]["mediaType"]
  120. and "tag" in event["target"]
  121. ):
  122. repo = event["target"]["repository"]
  123. tag = event["target"]["tag"]
  124. host = event["request"]["host"]
  125. user = event["actor"]["name"]
  126. reportbot_announce(
  127. f"\002[registry]\002 New manifest pushed to {host}/{repo}:{tag} by {user}"
  128. )
  129. return "", 204
  130. app.run("0.0.0.0")