Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 80ebbc2

Browse files
committed
v0.8.0
1 parent 543e566 commit 80ebbc2

23 files changed

+400
-421
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# CHANGELOG
22

3+
## v0.8.0 (2020-12-20)
4+
5+
* Refactored the `image` module and added unit tests
6+
* Added a fallback variable for `MODE` set to `production`
7+
* Created `conftest` file for test suite, started shifting fixtures around
8+
* Bumped Docker API version from `1.40` to `1.41`, there should be no change in behavior
9+
* Fixed a bug where if a container didn't exist yet, it would still try to wait, stop, and remove it on the deploy stage. The output would also blow up as it was impossible to do because it didn't exist. Now we check if a container exists prior to running those commands on the deploy stage and skip if no container exists
10+
* Various bug fixes and optimizations
11+
312
## v0.7.0 (2020-10-26)
413

514
* Refactored the `webhook` module and added unit tests

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ lint:
3232
venv/bin/flake8 harvey/*.py
3333
venv/bin/flake8 test/*.py
3434

35-
## test - Test the project
35+
## test - Test the project (unit tests)
3636
test:
3737
venv/bin/pytest
3838

39+
## integration test - Test the project (integration tests)
40+
integration_test:
41+
venv/bin/python test/integration/test_pipeline.py
42+
3943
## coverage - Test the project and generate an HTML coverage report
4044
coverage:
4145
venv/bin/pytest --cov=harvey --cov-branch --cov-report=html

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Environment Variables:
102102
SLACK_CHANNEL The Slack channel to send messages to
103103
SLACK_BOT_TOKEN The Slackbot token to use to authenticate each request to Slack
104104
WEBHOOK_SECRET The Webhook secret required by GitHub (if enabled) to secure your webhooks
105-
MODE Set to "test" to bypass the header and auth data from GitHub to test
105+
MODE Set to "test" to bypass the header and auth data from GitHub to test. Default: production
106106
HOST The host Harvey will run on. Default: 127.0.0.1
107107
PORT The port Harvey will run on. Default: 5000
108108
DEBUG Whether the Flask API will run in debug mode or not

docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
- Each `harvey.json` config file will house information about your tests and build/deploy
1515
- This file must follow proper JSON standards (start and end with `{ }`, contain commas after each item, no trailing commas, and be surrounded by quotes)
1616

17-
The following example will run a full pipeline (tests, build and deploy), tag it with a unique name based on the GitHub project. Provide the language and version for the test stage:
17+
The following example will run a full pipeline (tests, build and deploy), tag it with a unique name based on the GitHub project. Provide the language and version for the test stage.
18+
19+
**Note:** All keys must be lowercase!
1820

1921
```javascript
2022
{

examples/examples.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44

55

66
# """Retrieve a single image"""
7-
# image = harvey.Image.retrieve('harvey/python-test')
7+
# image = harvey.Image.retrieve_image('harvey/python-test')
88
# print(image)
99

1010
# """Retrieve a list of images"""
11-
# images = harvey.Image.all()
11+
# images = harvey.Image.retrieve_all_images()
1212
# print(json.dumps(images, indent=4))
1313

1414
# """Remove an image"""
15-
# remove_image = harvey.Image.remove('c1d7538e38f74ea6ba43920eaabd27b8')
15+
# remove_image = harvey.Image.remove_image('c1d7538e38f74ea6ba43920eaabd27b8')
1616
# print(remove_image)
1717

1818
# """Build an image"""
19-
# image = harvey.Image.build(
19+
# image = harvey.build_image(
2020
# config={
2121
# 'language': 'python',
2222
# 'version': '3.7',

harvey/app.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def start_pipeline_compose():
4545
def retrieve_pipeline(pipeline_id):
4646
"""Retrieve a pipeline's logs by ID
4747
"""
48-
# This is a hacky temporary solution until we can store
49-
# this data in a database and is not meant to remain
48+
# TODO: This is a hacky temporary solution until we can
49+
# store this data in a database and is not meant to remain
5050
# as a long-term solution
5151
file = f'{pipeline_id}.log'
5252
for root, dirs, files in os.walk(Global.PROJECTS_LOG_PATH):
@@ -61,8 +61,8 @@ def retrieve_pipeline(pipeline_id):
6161
def retrieve_pipelines():
6262
"""Retrieve a list of pipelines
6363
"""
64-
# This is a hacky temporary solution until we can store
65-
# this data in a database and is not meant to remain
64+
# TODO: This is a hacky temporary solution until we can
65+
# store this data in a database and is not meant to remain
6666
# as a long-term solution
6767
pipelines = {'pipelines': []}
6868
for root, dirs, files in os.walk(Global.PROJECTS_LOG_PATH, topdown=True):
@@ -144,28 +144,28 @@ def retrieve_pipelines():
144144
# data = json.loads(request.data)
145145
# tag = json.loads(request.tag)
146146
# context = json.loads(request.context)
147-
# build = harvey.Image.build(data, tag, context)
147+
# build = harvey.build_image(data, tag, context)
148148
# return build
149149

150150

151151
# @API.route('/images/<image_id>', methods=['GET'])
152152
# def retrieve_image(image_id):
153153
# """Retrieve a Docker image"""
154-
# response = json.dumps(harvey.Image.retrieve(image_id))
154+
# response = json.dumps(harvey.Image.retrieve_image(image_id))
155155
# return response
156156

157157

158158
# @API.route('/images', methods=['GET'])
159159
# def all_images():
160160
# """Retrieve all Docker images"""
161-
# response = json.dumps(harvey.Image.all())
161+
# response = json.dumps(harvey.Image.retrieve_all_images())
162162
# return response
163163

164164

165165
# @API.route('/images/<image_id>/remove', methods=['DELETE'])
166166
# def remove_image(image_id):
167167
# """Remove (delete) a Docker image"""
168-
# remove = harvey.Image.remove(image_id)
168+
# remove = harvey.Image.remove_image(image_id)
169169
# response = str(remove)
170170
# return response
171171

harvey/globals.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import os
2+
3+
14
class Global():
25
"""Contains global configuration for Harvey
36
"""
4-
DOCKER_VERSION = 'v1.40' # Docker API version
7+
DOCKER_VERSION = 'v1.41' # Docker API version
58
# TODO: Figure out how to sync this version number with the one in `setup.py`
6-
HARVEY_VERSION = '0.7.0' # Harvey release
9+
HARVEY_VERSION = '0.8.0' # Harvey release
710
PROJECTS_PATH = 'projects'
811
PROJECTS_LOG_PATH = 'logs/projects'
912
HARVEY_LOG_PATH = 'logs/harvey'
@@ -12,6 +15,7 @@ class Global():
1215
BASE_URL = f'http+unix://%2Fvar%2Frun%2Fdocker.sock/{DOCKER_VERSION}/'
1316
JSON_HEADERS = {'Content-Type': 'application/json'}
1417
TAR_HEADERS = {'Content-Type': 'application/tar'}
18+
APP_MODE = os.getenv('MODE', 'production').lower()
1519

1620
@classmethod
1721
def repo_name(cls, webhook):

harvey/image.py

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import os
32
import subprocess
43
import requests
@@ -7,8 +6,8 @@
76

87
class Image():
98
@classmethod
10-
def build(cls, config, webhook, context=''):
11-
"""Build a Docker image
9+
def build_image(cls, config, webhook, context=''):
10+
"""Build a Docker image by shelling out and running Docker commands.
1211
"""
1312
# TODO: Use the Docker API for building instead of a shell \
1413
# command (haven't because I can't get it working)
@@ -17,61 +16,53 @@ def build(cls, config, webhook, context=''):
1716
# data = requests.post(Global.BASE_URL + 'build', \
1817
# params=json, data=tar, headers=Global.TAR_HEADERS)
1918

20-
# Global variables
21-
if "dockerfile" in config:
22-
dockerfile = f'-f {config["dockerfile"]}'
23-
else:
24-
dockerfile = ''
25-
2619
# Set variables based on the context (test vs deploy vs full)
2720
if context == 'test':
21+
language = f'--build-arg LANGUAGE={config.get("language", "")}'
22+
version = f'--build-arg VERSION={config.get("version", "")}'
2823
project = f'--build-arg PROJECT={Global.repo_full_name(webhook)}'
29-
path = Global.PROJECTS_PATH
30-
else:
31-
project = ''
32-
path = os.path.join(Global.PROJECTS_PATH,
33-
Global.repo_full_name(webhook))
34-
35-
tag_arg = f'-t {Global.docker_project_name(webhook)}'
36-
37-
# For testing only:
38-
if "language" in config and context == 'test':
39-
language = f'--build-arg LANGUAGE={config["language"]}'
24+
path = f'{Global.PROJECTS_PATH}'
4025
else:
4126
language = ''
42-
if "version" in config and context == 'test':
43-
version = f'--build-arg VERSION={config["version"]}'
44-
else:
4527
version = ''
28+
project = ''
29+
path = os.path.join(Global.PROJECTS_PATH, Global.repo_full_name(webhook))
30+
dockerfile = f'-f {config["dockerfile"]}' if config.get("dockerfile") else ''
31+
tag_arg = f'-t {Global.docker_project_name(webhook)}'
4632

47-
# Build the image (exceptions handled at stage level)
33+
# Build the image (exceptions bubble up to the stage module)
34+
# We cd into the directory here so we have access to the files to copy into the container
4835
image = subprocess.check_output(
4936
f'cd {path} && docker build {dockerfile} {tag_arg} {language} {version} {project} .',
50-
stdin=None, stderr=None, shell=True, timeout=Global.BUILD_TIMEOUT)
37+
stdin=None,
38+
stderr=None,
39+
shell=True,
40+
timeout=Global.BUILD_TIMEOUT
41+
)
5142

5243
return image.decode('UTF-8')
5344

5445
@classmethod
55-
def retrieve(cls, image_id):
46+
def retrieve_image(cls, image_id):
5647
"""Retrieve a Docker image
5748
"""
58-
data = requests.get(Global.BASE_URL + f'images/{image_id}/json')
59-
return data.json()
49+
response = requests.get(Global.BASE_URL + f'images/{image_id}/json')
50+
return response
6051

6152
@classmethod
62-
def all(cls):
53+
def retrieve_all_images(cls):
6354
"""Retrieve all Docker images
6455
"""
65-
data = requests.get(Global.BASE_URL + 'images/json')
66-
return data.json()
56+
response = requests.get(Global.BASE_URL + 'images/json')
57+
return response
6758

6859
@classmethod
69-
def remove(cls, image_id):
60+
def remove_image(cls, image_id):
7061
"""Remove (delete) a Docker image
7162
"""
72-
data = requests.delete(
63+
response = requests.delete(
7364
Global.BASE_URL + f'images/{image_id}',
74-
data=json.dumps({'force': True}),
65+
json={'force': True},
7566
headers=Global.JSON_HEADERS
7667
)
77-
return data
68+
return response

harvey/pipeline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class Pipeline():
1010
@classmethod
1111
def test(cls, config, webhook, output):
12-
"""Pull changes and run tests (no deploy)
12+
"""Pull changes and run tests (will not deploy code)
1313
"""
1414
start_time = datetime.now()
1515
if os.getenv('SLACK'):

harvey/stage.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test(cls, config, webhook, output):
1919

2020
# Build the image
2121
try:
22-
image = Image.build(config, webhook, context)
22+
image = Image.build_image(config, webhook, context)
2323
image_output = f'Test image created.\n{image}'
2424
print(image_output)
2525
except subprocess.TimeoutExpired:
@@ -40,7 +40,7 @@ def test(cls, config, webhook, output):
4040
else:
4141
final_output = output + image_output + \
4242
'\nError: Harvey could not create the Test container.'
43-
Image.remove(test_project_name)
43+
Image.remove_image(test_project_name)
4444
Utils.kill(final_output, webhook)
4545

4646
# Start the container
@@ -51,7 +51,7 @@ def test(cls, config, webhook, output):
5151
else:
5252
final_output = output + image_output + container_output + \
5353
'\nError: Harvey could not start the container.'
54-
Image.remove(image[0])
54+
Image.remove_image(image[0])
5555
Container.remove_container(test_project_name)
5656
Utils.kill(final_output, webhook)
5757

@@ -63,7 +63,7 @@ def test(cls, config, webhook, output):
6363
else:
6464
final_output = output + image_output + container_output + start_output + \
6565
'\nError: Harvey could not wait for the container.'
66-
Image.remove(image[0])
66+
Image.remove_image(image[0])
6767
Container.remove_container(test_project_name)
6868
Utils.kill(final_output, webhook)
6969

@@ -77,21 +77,21 @@ def test(cls, config, webhook, output):
7777
else:
7878
final_output = output + image_output + container_output + start_output + wait_output + \
7979
'\nError: Harvey could not create the container logs.'
80-
Image.remove(image[0])
80+
Image.remove_image(image[0])
8181
Container.remove_container(test_project_name)
8282
Utils.kill(final_output, webhook)
8383

8484
# Remove container and image after it's done
8585
remove = Container.remove_container(test_project_name)
8686
if remove is not False:
87-
Image.remove(image[0])
87+
Image.remove_image(image[0])
8888
remove_output = 'Test container and image removed.'
8989
print(remove_output)
9090
else:
9191
final_output = output + image_output + container_output + start_output + \
9292
wait_output + logs_output + \
9393
'\nError: Harvey could not remove the container and/or image.'
94-
Image.remove(image[0])
94+
Image.remove_image(image[0])
9595
Container.remove_container(test_project_name)
9696
Utils.kill(final_output, webhook)
9797

@@ -110,8 +110,8 @@ def build(cls, config, webhook, output):
110110

111111
# Build the image
112112
try:
113-
Image.remove(Global.docker_project_name(webhook))
114-
image = Image.build(config, webhook)
113+
Image.remove_image(Global.docker_project_name(webhook))
114+
image = Image.build_image(config, webhook)
115115
image_output = f'Project image created\n{image}'
116116
print(image_output)
117117
except subprocess.TimeoutExpired:
@@ -146,9 +146,9 @@ def deploy(cls, webhook, output):
146146
stop_output = f'Stopping old {Global.docker_project_name(webhook)} container.'
147147
print(stop_output)
148148
elif stop_container.status_code == 304:
149-
# TODO: Add missing logic here
150-
stop_output = f'Error: {Global.docker_project_name(webhook)} is already stopped.'
151-
print(stop_output)
149+
stop_output = ''
150+
elif stop_container.status_code == 404:
151+
stop_output = ''
152152
else:
153153
# TODO: Add missing logic here
154154
stop_output = 'Error: Harvey could not stop the container.'
@@ -157,6 +157,8 @@ def deploy(cls, webhook, output):
157157
if wait_container.status_code == 200:
158158
wait_output = f'Waiting for old {Global.docker_project_name(webhook)} container to exit...'
159159
print(wait_output)
160+
elif wait_container.status_code == 404:
161+
wait_output = ''
160162
else:
161163
# TODO: Add missing logic here
162164
print(
@@ -166,6 +168,8 @@ def deploy(cls, webhook, output):
166168
if remove_container.status_code == 204:
167169
remove_output = f'Removing old {Global.docker_project_name(webhook)} container.'
168170
print(remove_output)
171+
elif remove_container.status_code == 404:
172+
remove_output = ''
169173
else:
170174
# TODO: Add missing logic here
171175
print(

0 commit comments

Comments
 (0)