|
3 | 3 | from datetime import datetime
|
4 | 4 | import hmac
|
5 | 5 | import hashlib
|
6 |
| -from flask import abort |
7 | 6 | from threading import Thread
|
8 | 7 | from harvey.pipeline import Pipeline
|
9 | 8 | from harvey.git import Git
|
10 | 9 | from harvey.globals import Global
|
11 | 10 | from harvey.utils import Utils
|
12 | 11 |
|
13 | 12 |
|
| 13 | +WEBHOOK_SECRET = os.getenv('WEBHOOK_SECRET') |
| 14 | +APP_MODE = os.getenv('MODE') |
| 15 | + |
| 16 | + |
14 | 17 | class Webhook():
|
15 | 18 | @classmethod
|
16 |
| - def init(cls, webhook): |
| 19 | + def initialize_pipeline(cls, webhook): |
17 | 20 | """Initiate the logic for webhooks and pull the project
|
18 | 21 | """
|
19 | 22 | start_time = datetime.now()
|
20 |
| - preamble = f'Running Harvey v{Global.HARVEY_VERSION}\n' + \ |
21 |
| - f'Pipeline Started: {start_time}' |
| 23 | + preamble = f'Running Harvey v{Global.HARVEY_VERSION}\nPipeline Started: {start_time}' |
22 | 24 | pipeline_id = f'Pipeline ID: {Global.repo_commit_id(webhook)}\n'
|
23 | 25 | print(preamble)
|
24 |
| - git_message = (f'New commit by: {Global.repo_commit_author(webhook)}. \ |
25 |
| - \nCommit made on repo: {Global.repo_full_name(webhook)}.') |
| 26 | + git_message = (f'New commit by: {Global.repo_commit_author(webhook)}.' |
| 27 | + f'\nCommit made on repo: {Global.repo_full_name(webhook)}.') |
26 | 28 | git = Git.update_git_repo(webhook)
|
| 29 | + config = cls.open_project_config(webhook) |
| 30 | + execution_time = f'Startup execution time: {datetime.now() - start_time}\n' |
| 31 | + output = (f'{preamble}\n{pipeline_id}Configuration:\n{json.dumps(config, indent=4)}' |
| 32 | + f'\n\n{git_message}\n{git}\n{execution_time}') |
| 33 | + print(execution_time) |
| 34 | + return config, output |
27 | 35 |
|
28 |
| - # Open the project's config file to assign pipeline variables |
| 36 | + @classmethod |
| 37 | + def open_project_config(cls, webhook): |
| 38 | + """Open the project's config file to assign pipeline variables |
| 39 | + """ |
| 40 | + # TODO: Add the ability to configure projects on the Harvey side |
| 41 | + # instead of only from within a JSON file in the repo |
29 | 42 | try:
|
30 |
| - filename = os.path.join(Global.PROJECTS_PATH, Global.repo_full_name(webhook), |
31 |
| - 'harvey.json') |
| 43 | + filename = os.path.join( |
| 44 | + Global.PROJECTS_PATH, Global.repo_full_name(webhook), 'harvey.json' |
| 45 | + ) |
32 | 46 | with open(filename, 'r') as file:
|
33 | 47 | config = json.loads(file.read())
|
34 | 48 | print(json.dumps(config, indent=4))
|
35 |
| - except FileNotFoundError as fnf_error: |
36 |
| - final_output = f'Error: Harvey could not find "harvey.json" file in \ |
37 |
| - {Global.repo_full_name(webhook)}.' |
38 |
| - print(fnf_error) |
| 49 | + return config |
| 50 | + except FileNotFoundError: |
| 51 | + final_output = f'Error: Harvey could not find a "harvey.json" file in {Global.repo_full_name(webhook)}.' |
| 52 | + print(final_output) |
39 | 53 | Utils.kill(final_output, webhook)
|
40 | 54 |
|
41 |
| - execution_time = f'Startup execution time: {datetime.now() - start_time}\n' |
42 |
| - output = f'{preamble}\n{pipeline_id}Configuration:\n{json.dumps(config, indent=4)}' + \ |
43 |
| - f'\n\n{git_message}\n{git}\n{execution_time}' |
44 |
| - print(execution_time) |
45 |
| - |
46 |
| - return config, output |
47 |
| - |
48 | 55 | @classmethod
|
49 |
| - def parse_webhook(cls, request, target): |
| 56 | + def parse_webhook(cls, request, use_compose): |
50 | 57 | """Initiate details to receive a webhook
|
51 | 58 | """
|
52 |
| - data = request.data |
| 59 | + success = False |
| 60 | + message = 'Server-side error.' |
| 61 | + status_code = 500 |
| 62 | + data = request.json if request.json else None |
53 | 63 | signature = request.headers.get('X-Hub-Signature')
|
54 |
| - parsed_data = json.loads(data) # TODO: Is this necessary? |
55 |
| - # TODO: Allow the user to configure whatever branch they'd like to pull from |
56 |
| - if parsed_data['ref'] in ['refs/heads/master', 'refs/heads/main']: |
57 |
| - if os.getenv('MODE') == 'test': |
58 |
| - Thread(target=target, args=(parsed_data,)).start() |
59 |
| - return "200" |
60 |
| - if cls.decode_webhook(data, signature): |
61 |
| - Thread(target=target, args=(parsed_data,)).start() |
62 |
| - return "200" |
63 |
| - return abort(403) |
64 |
| - return abort(500, 'Harvey can only pull from the "master" or "main" branch.') |
| 64 | + |
| 65 | + if request.data and data: |
| 66 | + # TODO: Allow the user to configure whatever branch they'd like to pull from |
| 67 | + if data['ref'] in ['refs/heads/master', 'refs/heads/main']: |
| 68 | + if APP_MODE == 'test' or cls.decode_webhook(data, signature): |
| 69 | + Thread(target=cls.start_pipeline, args=(data, use_compose,)).start() |
| 70 | + message = f'Started pipeline for {data["repository"]["name"]}' |
| 71 | + status_code = 200 |
| 72 | + success = True |
| 73 | + if APP_MODE != 'test' and not cls.decode_webhook(data, signature): |
| 74 | + message = 'The X-Hub-Signature did not match the WEBHOOK_SECRET.' |
| 75 | + status_code = 403 |
| 76 | + else: |
| 77 | + message = 'Harvey can only pull from the "master" or "main" branch of a repo.' |
| 78 | + status_code = 422 |
| 79 | + else: |
| 80 | + message = 'Malformed or missing JSON data in webhook.' |
| 81 | + status_code = 422 |
| 82 | + response = { |
| 83 | + 'success': success, |
| 84 | + 'message': message, |
| 85 | + }, status_code |
| 86 | + return response |
65 | 87 |
|
66 | 88 | @classmethod
|
67 | 89 | def decode_webhook(cls, data, signature):
|
68 | 90 | """Decode a webhook's secret key
|
69 | 91 | """
|
70 |
| - secret = bytes(os.getenv('WEBHOOK_SECRET'), 'UTF-8') |
71 |
| - mac = hmac.new(secret, msg=data, digestmod=hashlib.sha1) |
72 |
| - return hmac.compare_digest('sha1=' + mac.hexdigest(), signature) |
| 92 | + if signature: |
| 93 | + secret = bytes(WEBHOOK_SECRET, 'UTF-8') |
| 94 | + mac = hmac.new(secret, msg=data, digestmod=hashlib.sha1) |
| 95 | + return hmac.compare_digest('sha1=' + mac.hexdigest(), signature) |
| 96 | + else: |
| 97 | + return False |
73 | 98 |
|
74 | 99 | @classmethod
|
75 |
| - def start_pipeline(cls, webhook): |
| 100 | + def start_pipeline(cls, webhook, use_compose=False): |
76 | 101 | """Receive a webhook and spin up a pipeline based on the config
|
77 | 102 | """
|
78 |
| - webhook_config, webhook_output = Webhook.init(webhook) |
| 103 | + webhook_config, webhook_output = cls.initialize_pipeline(webhook) |
| 104 | + webhook_pipeline = webhook_config['pipeline'] |
79 | 105 |
|
80 |
| - if webhook_config['pipeline'] == 'test': |
81 |
| - pipeline = Pipeline.test(webhook_config, webhook, webhook_output) |
82 |
| - elif webhook_config['pipeline'] == 'deploy': |
83 |
| - pipeline = Pipeline.deploy(webhook_config, webhook, webhook_output) |
84 |
| - elif webhook_config['pipeline'] == 'full': |
85 |
| - pipeline = Pipeline.full(webhook_config, webhook, webhook_output) |
86 |
| - elif webhook_config['pipeline'] == 'pull': |
| 106 | + if webhook_pipeline == 'pull': |
87 | 107 | pipeline = Utils.success(webhook_output, webhook)
|
88 |
| - elif not webhook_config['pipeline']: |
89 |
| - final_output = webhook_output + '\nError: Harvey could not run, \ |
90 |
| - there was no pipeline specified.' |
91 |
| - Utils.kill(final_output, webhook) |
92 |
| - |
93 |
| - return pipeline |
94 |
| - |
95 |
| - @classmethod |
96 |
| - def start_pipeline_compose(cls, webhook): |
97 |
| - """Receive a webhook and spin up a pipeline based on the config |
98 |
| - """ |
99 |
| - webhook_config, webhook_output = Webhook.init(webhook) |
100 |
| - |
101 |
| - if webhook_config['pipeline'] == 'test': |
| 108 | + elif webhook_pipeline == 'test': |
102 | 109 | pipeline = Pipeline.test(webhook_config, webhook, webhook_output)
|
103 |
| - elif webhook_config['pipeline'] == 'deploy': |
104 |
| - pipeline = Pipeline.deploy_compose(webhook_config, webhook, webhook_output) |
105 |
| - elif webhook_config['pipeline'] == 'full': |
| 110 | + elif webhook_pipeline == 'deploy' and use_compose is False: |
| 111 | + pipeline = Pipeline.deploy(webhook_config, webhook, webhook_output) |
| 112 | + elif webhook_pipeline == 'full' and use_compose is True: |
106 | 113 | pipeline = Pipeline.full_compose(webhook_config, webhook, webhook_output)
|
107 |
| - elif webhook_config['pipeline'] == 'pull': |
108 |
| - pipeline = Utils.success(webhook_output, webhook) |
109 |
| - elif not webhook_config['pipeline']: |
110 |
| - final_output = webhook_output + '\nError: Harvey could not run, \ |
111 |
| - there was no pipeline specified.' |
112 |
| - Utils.kill(final_output, webhook) |
| 114 | + elif webhook_pipeline == 'deploy' and use_compose is True: |
| 115 | + pipeline = Pipeline.deploy_compose(webhook_config, webhook, webhook_output) |
| 116 | + elif webhook_pipeline == 'full' and use_compose is False: |
| 117 | + pipeline = Pipeline.full(webhook_config, webhook, webhook_output) |
| 118 | + else: |
| 119 | + final_output = webhook_output + '\nError: Harvey could not run, there was no acceptable pipeline specified.' |
| 120 | + pipeline = Utils.kill(final_output, webhook) |
113 | 121 |
|
114 | 122 | return pipeline
|
0 commit comments