├── .gitignore
├── requirements.txt
├── README.markdown
├── static
└── bigred.png
├── forms.py
├── settings.py.dist
├── templates
├── history.html
└── index.html
└── chief.py
/.gitignore:
--------------------------------------------------------------------------------
1 | settings.py
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask
2 | gunicorn
3 | redis
4 | WTForms
5 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | **Development has moved** to https://github.com/mozilla/chief.
2 |
--------------------------------------------------------------------------------
/static/bigred.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jbalogh/chief/HEAD/static/bigred.png
--------------------------------------------------------------------------------
/forms.py:
--------------------------------------------------------------------------------
1 | from wtforms import Form, TextField, PasswordField, validators
2 |
3 | import settings
4 |
5 |
6 | class DeployForm(Form):
7 | ref = TextField('git ref', [validators.Required()])
8 | password = PasswordField('secret', [validators.Required()])
9 | who = TextField('identify yourself', [validators.Required()])
10 |
--------------------------------------------------------------------------------
/settings.py.dist:
--------------------------------------------------------------------------------
1 | # Webapp Configs
2 | WEBAPPS = {
3 | 'addons.dev': {
4 | 'script': '/Users/jbalogh/dev/zamboni/scripts/update/update.py', # Path to commander scripts with pre_update, update, and deploy tasks
5 | 'pubsub_channel': 'deploy.amo', # The is the name of the channel where chief posts events
6 | 'password': '', # The app requires this secret before taking any action.
7 | },
8 | }
9 |
10 | # Directory where chief should redirect output. Make sure it exists.
11 | # Make sure Apache can read this dir so we can see the deploy output.
12 | OUTPUT_DIR = '/tmp'
13 |
14 | # Redis connection parameters. Everything is passed directly to redis.Redis, no
15 | # default settings are added.
16 | REDIS_BACKENDS = {
17 | 'master': {
18 | 'host': 'localhost',
19 | 'port': 6379,
20 | 'password': None,
21 | 'db': 0,
22 | 'socket_timeout': 0.1,
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/templates/history.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | CHIEF - {{ app_name }}: history
4 |
35 |
36 |
37 |
38 |
39 |
40 | | Date |
41 | User |
42 | Ref |
43 | Status |
44 | Log |
45 |
46 |
47 |
48 | {% for result in results %}
49 |
50 | | {{ result.datetime }} |
51 | {{ result.user }} |
52 | {{ result.ref }} |
53 | {{ result.status }} |
54 | view |
55 |
56 | {% endfor %}
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 | CHIEF - {{ app_name }}
2 |
58 | Hi, I'm the chief of {{ app_name }}.
59 | {% if form.errors or errors %}
60 |
61 | {% for field_name, field_errors in form.errors.iteritems() if field_errors %}
62 | {% for error in field_errors %}
63 | - {{ form[field_name].label }}: {{ error }}
64 | {% endfor %}
65 | {% endfor %}
66 | {% for error in errors %}
67 | - {{ error }}
68 | {% endfor %}
69 |
70 | {% endif %}
71 |
78 | history
79 |
--------------------------------------------------------------------------------
/chief.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import re
4 | import subprocess
5 | import time
6 |
7 | import redis as redislib
8 | from flask import Flask, Response, abort, request, render_template
9 |
10 | import settings
11 | from forms import DeployForm
12 |
13 |
14 | app = Flask(__name__)
15 |
16 | os.environ['PYTHONUNBUFFERED'] = 'go time'
17 |
18 |
19 | def do_update(app_name, app_settings, webapp_ref, who):
20 | deploy = app_settings['script']
21 | log_dir = os.path.join(settings.OUTPUT_DIR, app_name)
22 | timestamp = int(time.time())
23 | datetime = time.strftime("%b %d %Y %H:%M:%S", time.localtime())
24 | if not os.path.isdir(log_dir):
25 | os.mkdir(log_dir)
26 |
27 | log_name = "%s.%s" % (re.sub('[^A-z0-9_-]', '.', webapp_ref), timestamp)
28 | log_file = os.path.join(log_dir, log_name)
29 |
30 | def run(task, output):
31 | subprocess.check_call(['commander', deploy, task],
32 | stdout=output, stderr=output)
33 |
34 | def pub(event):
35 | redis = redislib.Redis(**settings.REDIS_BACKENDS['master'])
36 | d = {'event': event, 'ref': webapp_ref, 'who': who,
37 | 'logname': log_name}
38 | redis.publish(app_settings['pubsub_channel'], json.dumps(d))
39 |
40 | def history(status):
41 | redis = redislib.Redis(**settings.REDIS_BACKENDS['master'])
42 | d = {'timestamp':timestamp, 'datetime': datetime,
43 | 'status': status, 'user': who, 'ref': webapp_ref}
44 | key = "%s:%s" % (app_name, timestamp)
45 | redis.hmset(key, d)
46 |
47 | try:
48 | output = open(log_file, 'a')
49 |
50 | pub('BEGIN')
51 | yield 'Updating! revision: %s\n' % webapp_ref
52 |
53 | run('pre_update:%s' % webapp_ref, output)
54 | pub('PUSH')
55 | yield 'We have the new code!\n'
56 |
57 | run('update', output)
58 | pub('UPDATE')
59 | yield "Code has been updated locally!\n"
60 |
61 | run('deploy', output)
62 | pub('DONE')
63 | history('Success')
64 | yield 'All done!'
65 | except:
66 | pub('FAIL')
67 | history('Fail')
68 | raise
69 |
70 | def get_history(app_name, app_settings):
71 | redis = redislib.Redis(**settings.REDIS_BACKENDS['master'])
72 | results = []
73 | key_prefix = "%s:*" % app_name
74 | for history in redis.keys(key_prefix):
75 | results.append(redis.hgetall(history))
76 | return sorted(results, key=lambda k: k['timestamp'], reverse=True)
77 |
78 |
79 | @app.route("/", methods=['GET', 'POST'])
80 | def index(webapp):
81 | if webapp not in settings.WEBAPPS.keys():
82 | abort(404)
83 | else:
84 | app_settings = settings.WEBAPPS[webapp]
85 |
86 | errors = []
87 | form = DeployForm(request.form)
88 | if request.method == 'POST' and form.validate():
89 | if form.password.data == app_settings['password']:
90 | return Response(do_update(webapp, app_settings,
91 | form.ref.data, form.who.data),
92 | direct_passthrough=True,
93 | mimetype='text/plain')
94 | else:
95 | errors.append("Incorrect password")
96 |
97 | return render_template("index.html", app_name=webapp,
98 | form=form, errors=errors)
99 |
100 | @app.route("//history", methods=['GET'])
101 | def history(webapp):
102 | if webapp not in settings.WEBAPPS.keys():
103 | abort(404)
104 | else:
105 | app_settings = settings.WEBAPPS[webapp]
106 | results = get_history(webapp, app_settings)
107 | return render_template("history.html", app_name=webapp,
108 | results=results)
109 |
--------------------------------------------------------------------------------