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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import os
  2. from collections import deque
  3. from flask import Flask, abort, request, Response, render_template
  4. from functools import wraps
  5. from lineandsinker.common import get_hook_key
  6. from lineandsinker.services import services
  7. def refresh_services():
  8. for service in services.values():
  9. service.refresh()
  10. refresh_services()
  11. history = deque(maxlen=100)
  12. app = Flask(__name__)
  13. def handle_events(events):
  14. for event in events:
  15. history.append(event)
  16. if event["type"] == "git.push":
  17. services["jenkins"].build_job_by_scm_url(event["repo"]["urls"])
  18. if event["repo"]["public"]:
  19. services["irccat"].announce(
  20. f"\002[git]\002 {event['user']} pushed {len(event['commits'])} commit{'s' if len(event['commits']) != 1 else ''} to {event['repo']['name']}: {event['compare_url']}"
  21. )
  22. for commit in event["commits"][::-1][:3]:
  23. line = commit["message"].split("\n")[0][:100]
  24. services["irccat"].announce(f"\002[git]\002 {commit['id']}: {line}")
  25. elif event["type"] == "docker.push":
  26. services["irccat"].announce(
  27. f"\002[registry]\002 New manifest pushed to {event['host']}/{event['repo']}:{event['tag']} by {event['user']}"
  28. )
  29. elif event["type"] == "slack":
  30. services["irccat"].announce(f"\002[{event['source']}]\002 {event['text']}")
  31. elif event["type"] == "sensu":
  32. state = event["check"]["state"].upper()
  33. if state == "FAILING":
  34. state = f"\0034{state}\003"
  35. services["irccat"].announce(
  36. f"`{event['check']['metadata']['name']}` is now \002{state}\002\n{event['check']['output']}",
  37. "#sensu",
  38. )
  39. @app.route("/")
  40. def handle_index():
  41. return render_template("index.html")
  42. @app.route("/hooks/<service>/<path:identifier>/<hash>", methods=["GET", "POST"])
  43. def handle_hook(service, identifier, hash):
  44. app.logger.info(f"Received hook for {service} with identifier {hash}")
  45. expected_hash = get_hook_key(service, identifier)
  46. if hash != expected_hash:
  47. handle_events(
  48. [
  49. {
  50. "type": "lineandsinker.badhash",
  51. "service": service,
  52. "hash": hash,
  53. "body": request.get_data(as_text=True),
  54. }
  55. ]
  56. )
  57. app.logger.info(f"Hash not valid. Expected: {expected_hash}, got {hash}")
  58. abort(403)
  59. if service not in services:
  60. handle_events(
  61. [
  62. {
  63. "type": "lineandsinker.badservice",
  64. "service": service,
  65. "known_services": list(services.keys()),
  66. "body": request.get_data(as_text=True),
  67. }
  68. ]
  69. )
  70. app.logger.info(f"Unknown service {service}, known: {services.keys()}")
  71. abort(404)
  72. handle_events(services[service].accept_hook(identifier, request) or [])
  73. return "", 200
  74. if "LAS_ADMIN_PASSWORD" in os.environ:
  75. def requires_auth(f):
  76. @wraps(f)
  77. def decorated(*args, **kwargs):
  78. auth = request.authorization
  79. if (
  80. not auth
  81. or auth.username != "admin"
  82. or auth.password != os.environ["LAS_ADMIN_PASSWORD"]
  83. ):
  84. return Response(
  85. "Restricted resource",
  86. 401,
  87. {"WWW-Authenticate": 'Basic realm="Login Required"'},
  88. )
  89. return f(*args, **kwargs)
  90. return decorated
  91. @app.route("/admin")
  92. @requires_auth
  93. def handle_admin():
  94. return render_template("admin.html", events=history)
  95. @app.route("/admin/hash", methods=["POST"])
  96. @requires_auth
  97. def handle_admin_hash():
  98. return get_hook_key(request.form["service"], request.form["identifier"]), 200
  99. if __name__ == "__main__":
  100. app.run("0.0.0.0")