├── .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 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for result in results %} 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% endfor %} 57 | 58 |
DateUserRefStatusLog
{{ result.datetime }}{{ result.user }}{{ result.ref }}{{ result.status }}view
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 | 70 | {% endif %} 71 |
72 | {{ form.who(placeholder=form.who.name) }} 73 | {{ form.password(placeholder=form.password.name) }} 74 | {{ form.ref(placeholder=form.ref.name) }} 75 |
76 | 77 |
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 | --------------------------------------------------------------------------------