En el primer artículo de la serie vimos como crear un repositorio público (podríamos haberlo hecho privado perfectamente) en Bitbucket y registramos un servicio de POST para el mismo.
También comprobamos que el servicio estaba funcionando y que el objeto JSON enviado que contenía la información de nuestro repositorio y commit estaba bien formado y era correcto.
Hoy vamos a hacer una primera prueba sencilla de auto-deploy previa batería de tests para que el lector pueda ir haciéndose una ligera idea de lo que podemos hacer con muy poco esfuerzo.
Pero antes de nada, debemos escribir el servicio que va a encargarse de capturar ese POST enviado por Bitbucket y realizar las operaciones adecuadas. Este servicio se ejecutará en la máquina destino a la que tenemos acceso por SSH.
Servicio de Despliegue
El servicio de despliegue de nuestra aplicación está programado en Python utilizando las librerías Twisted (tal y como hemos hecho con el código de la aplicación en si) pero podría estar escrito en cualquier otro lenguaje.
Nuestro servicio es muy sencillo, escucha conexiones entrantes por el puerto 1936 y delega todo el trabajo a un módulo que se encarga de parsear la petición, ejecutar los tests y realizar el deploy, vamos a verlo:
from twisted.web import server, resource from deployer import worker class DeploymentResource(resource.Resource): """ Process the POST petition and skip the GET ones I use twistd service to run, I'm a RPY Script """ isLeaf = True def render_GET(self, request): return "Nothing to see here!" def render_POST(self, request): deployer_worker = worker.Deployer(request) return server.NOT_DONE_YET resource = DeploymentResource()
El anterior archivo crea un objeto de tipo twisted.web.resource.Resource
que procesa la petición HTTP proveniente de Bitbucket. No usamos un reactor de Twisted por que es un script de recursos de Twisted y se usa con la utilidad de línea de comandos twistd
.
Como podéis apreciar, todo el trabajo es delegado a un worker que vamos a definir en su propio package (deployer) que va a encargarse de hacer el trabajo duro:
# -*- test-case-name: deployer.test_deployer -*- import json import subprocess import os class Deployer(object): def __init__(self, request): self._request = request self._data = json.loads(request.args['payload'][0]) html = [ '<!doctype html>', '<html>', ' <head>', ' <title>Genbetadev Auto Deployer</title>', ' <style type="text/css">', ' .error { color: red; }', ' .success { color: green; }', ' </style>', ' </head>', ' <body>' ] self._request.write("\n".join(html)) self._request.write('<br />Executing tests...') self._check() def _check(self): if self._check_tests(): self._request.write('<br />%s' % self._out.replace("\n", "<br />")) print self._out self._request.write('<br /><span class="cuccess">All tests passed... deploying</span>') print 'All tests passed... deploying' self._deploy(); else: print self._out self._request.write('<span class="error">Tests failed</span>:<br />%s<br /><br /><b class="error">Deploy Cancelled</b>' % self._out.replace("\n", "<br />")) print 'Tests failed... not deploying' self._request.write(' </body>'); self._request.write('</html>'); self._request.finish() def _check_tests(self): process = subprocess.Popen(['trial', 'deployer'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self._out, self._error = process.communicate() if process.returncode == 0: return True else: return False def _deploy(self): currpwd = os.getcwd() os.chdir('/var/www/localhost/htdocs/deploytest') process = subprocess.Popen(['git', 'pull'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) self._out, self._error = process.communicate() os.chdir(currpwd) print self._out self._request.write('<br /><br />%s' % self._out.replace("\n", "<br />")) if process.returncode == 0: process = subprocess.Popen(['']) self._request.write('<br /><b class="error">Project Deployed</b>') else: self._request.write('<br /><b class="error">Project not Deployed</b>')
NOTA: Un copy and paste de este código seguramente no te funcione a no ser que mantengas las mismas rutas, esto no pretende ser un tutorial paso a paso sino una explicación de como realizar un auto deployment con Bitbucket.
Este archivo es algo más largo y se encarga de realizar todo el trabajo efectivo de testeo y deployment de nuestro código. Como podéis observar, nuestro worker ejecuta una batería de tests con trial
y si se pasan todos los tests, entonces procede a realizar el despliegue.
Tests
Vamos a escribir el código de los tests, para ello debemos crear un nuevo package tests
dentro de package deployer:
$ mkdir deployer/tests $ touch deployer/tests/__init__.py
Dentro de ese package podemos crear nuestros archivos de tests que serán ejecutados por trial
antes de desplegar nuestros commits, este va a llamarse test_worker.py
:
from twisted.trial import unittest class WorkerTest(unittest.TestCase): def test_worker(self): self.assertEqual(1, 2)
Este test siempre fallará ya que 1 nunca va a ser igual a 2, lo dejamos así para comprobar que nuestro worker trabaja como se espera de él.
KISS
Para facilitar las cosas, vamos a modificar el código al que llamamos webservice.py
en el artículo anterior para convertirlo en un script de recursos de Twisted:
from twisted.web import server, resource class HelloResource(resource.Resource): isLeaf = True numberRequests = 0 def render_GET(self, request): self.numberRequests += 1 request.setHeader("content-type", "text/plain") return "I am request #" + str(self.numberRequests) + "\n" resource = HelloResource()
Es necesario renombrar el archivo a webservice.rpy
para que sea interpretado por twistd
. Y no olvidemos subir los cambios al repositorio git, esto enviará otro POST a nuestro PostBin, agua que no has de beber...
Pues bien, ya tenemos todos los elementos para probar el servicio. Lo primero que tenemos que hacer es volver a las opciones de administración de nuestro repositorio y cambiar la URL a donde se debe dirigir el POST para que apunte a nuestro servicio. En mi caso sería algo como: http://xxx.xxx.xxx.xxx:1936/deployer.rpy
Vamos a clonar el repositorio git de nuestra aplicación en la máquina destino y a hacer nuestra prueba:
~ cd /var/www/localhost/htdocs $ git clone https://bitbucket.org/damnwidget/deploytest.git
Hemos clonado el repositorio público por HTTP, también podíamos haber clonado un repositorio privado (y hacer deploy de él) utilizando una key SSH. Revisa la documentación de Bitbucket si quieres saber más sobre ello.
Levantamos los servicios con twisted:
$ cd /var/www/localhost/htdocs/deploytest $ twistd web --path ./ --port 8080 cd /var/www/localhost/htdocs/deployer_service $ twistd web --path ./ --port 1936
Esto levanta dos reactores con twisted uno con el servicio a desplegar que escucha en el puerto 8080 y el servicio que realiza los tests y el despliegue en el 1936, ya solo nos queda hacer un cambio al repositorio y ver si esto funciona.
Para ello vamos a ir a lo simple y vamos a crear un nuevo archivo vacío en el repositorio:
$ touch deleteme git add deleteme git commit -m "File 'deleteme' added to tree for testing purposes" git push
En el log de nuestro servicio de despliegue obtenemos la siguiente petición:
2012-02-17 21:59:37+0100 [HTTPChannel,0,63.246.22.222] 63.246.22.222 - - [17/Feb/2012:20:59:36 +0000] "POST /deployer.rpy HTTP/1.1" 200 628 "-" "Bitbucket.org"
y un extenso log de como ha ido la ejecución de los tests.
Como podemos comprobar, han fallado puesto que uno nunca puede ser igual a dos y eso es lo que se está testeando. Si vuestro test ha pasado y no ha dado fail es debido a que en la configuración de vuestra instalación de Python no se añade el directorio actual en la ruta de Python, para corregir esto debéis levantar el servicio de despliegue de la siguiente manera:
$ PYTHONPAHT=$(pwd) twistd web --path ./ --port 1936
De esta manera nos aseguramos de que el comando trial
encuentra el módulo deployer
para realizar los tests.
Ahora que hemos comprobado que el servicio está funcionando correctamente, vamos a modificar el test para que trial lo pase y vamos a introducir un cambio en nuestro webservice.rpy
para comprobar que efectivamente nuestra página web se actualiza a la última versión del repositorio.
Si apuntamos nuestro navegador a http://nuestra_url:8080/webservice.rpy nos encontraremos con una página que dice "I am request #1", vamos a hacer un ligero cambio y hacer un commit para comprobar que todo funciona. Primero modificamos el test en el servicio de despliegue para que pase, en la línea 7 de test_worker.py:
self.assertEqual(1, 1)
El test pasará ya que uno siempre es igual a uno, vamos a proceder pues a modificar nuestro webservice.rpy
. Ahora el nuevo código de nuestra página:
from twisted.web import server, resource class HelloResource(resource.Resource): isLeaf = True numberRequests = 0 def render_GET(self, request): self.numberRequests += 1 request.setHeader("content-type", "text/plain") return "Hello, Genbetadev\n" resource = HelloResource()
Antes de realizar el commit, es conveniente matar el proceso twistd de nuestro deployer y volverlo a levantar:
kill -9 $(cat twistd.pid) && PYTHONPATH=$(pwd) twistd web --path=./ --port=1936
Al hacer push de nuestro código esta vez el log reflejará lo siguiente:
2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] deployer.tests.test_deployer 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] WorkerTest 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] test_worker ... [OK] 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] ------------------------------------------------------------------------------- 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] Ran 1 tests in 0.002s 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] PASSED (successes=1) 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] All tests passed... deploying 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] Updating e3d26b8..25619d1 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] Fast-forward 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] webservice.rpy | 12 ++++++++++++ 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] 1 files changed, 12 insertions(+), 0 deletions(-) 2012-02-17 22:28:37+0100 [HTTPChannel,0,95.20.174.101] create mode 100644 webservice.rpy
Si recargamos la página web obtendremos el siguiente mensaje:
Hello, Genbetadev
Espero que toda esta explicación se haya entendido porque creo que realmente es más sencillo hacerlo que explicarlo, es una forma muy sencilla y trivial de poder desplegar nuestras aplicaciones o páginas web en producción o testeo de forma sencilla.
El lector debe notar que esta no es la forma correcta de pasar un test, si nos fijamos, el test se lo estamos pasando al servicio de despliegue y no al servicio web, lo lógico en una aplicación real sería que el test se lo pasemos a aquello que vamos a desplegar, no al servicio de depliegue.
Tampoco hemos hecho nada útil con el JSON que nos da Bitbucket, en próximas entregas de la serie crearemos un auténtico servicio de auto despliegue con aplicación real.
Hasta entonces, happy hacking!
Más en Genbetadev | Hacer auto deploy con bitbucket y git (parte 1), Sistemas de Control de Versiones