Пишем простой buildserver на Python (Часть 2)
от Screamer
Фронтенд будет написан с помощью AngularJS.
В директории web, что располагается в корне проекта создайте директорий templates и файлы application.js, index.html, style.css
templates/home.html -- Домашняя страница
templates/project.html -- Страница просмотра проекта
templates/build.html -- Страница просмотра сборки
templates/project-form.html -- Форма создания/редактирования проекта
templates/project-remove.html -- Форма удаления проекта
templates/error.html -- Сообщение об ошибке
С фронтендом почти покончено.
Обновляем setup.py:
Выполняем bin/buildout, после чего будут сгенерированы bin/broadcast, bin/web и bin/worker
Запускаем их.
Создаём тестовый проект:
$ mkdir /tmp/testrepo && cd /tmp/testrepo && git init
$ editor .buildserver
Пишем туда следующее:
$ git add .buildserver && git commit -m "Init"
Открываем в браузере http://buildserver добавляем проект. В качестве урлы указываем /tmp/testrepo.
После сохранения проекта жмём Build, обновляем страницу и переходим к просмотру лога.
Если я нигде не ошибся и вы всё сделали правильно, то всё должно быть ok.
Ну а теперь домашнее задание. Гг.
Сделать отображение уведомления о том, что билд был добавлен в очередь на сборку.
Обновлять список сборок в режиме реального времени (добавление/изменение/удаление).
Причём не ддосить базу запросами, а задействовать zmq и tornado.
На этом всё. Исходный код проекта можно найти здесь: https://github.com/Kilte/buildserver
В директории web, что располагается в корне проекта создайте директорий templates и файлы application.js, index.html, style.css
index.html
index.html
- <!DOCTYPE html>
- <html ng-app="app"><!-- Говорим, что это корневой элемент для модуля под названием app -->
- <head>
- <meta http-equiv="content-type" content="text/html" charset="utf-8" />
- <link rel="stylesheet" href="/style.css" type="text/css"/>
- <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
- <title>BuildServer</title>
- </head>
- <body>
- <ul class="nav">
- <li><a href="#/">BuildServer</a></li>
- <li><a href="#/new-project">New project</a></li>
- </ul>
- <div class="content" ng-view></div> <!-- Здесь будет отображаться содержимое шаблонов -->
- <script type="text/javascript" src="/vendor/angular/angular.js"></script>
- <script type="text/javascript" src="/vendor/angular-route/angular-route.js"></script>
- <script type="text/javascript" src="/vendor/angular-websocket/angular-websocket.js"></script>
- <script type="text/javascript" src="/application.js"></script>
- </body>
- </html>
style.css
style.css
- body {
- background: #202020;
- color: #888888;
- padding: 0;
- margin: 0;
- }
- a {
- color: #fd5a4b;
- text-decoration: none;
- }
- a:hover {
- border-bottom: 1px dotted #fd5a4b;
- }
- ul.nav {
- list-style: none;
- padding: 15px 17px;
- margin: 0 0 20px 0;
- border-bottom: 1px solid #303030;
- box-shadow: inset 0 0 8px #000000;
- font-variant: small-caps;
- }
- ul.nav > li {
- display: inline-block;
- margin: 0 10px;
- padding: 0;
- }
- .content {
- max-width: 80%;
- margin: 0 auto;
- }
- .title {
- color: #fd5a4b;
- font-size: 14pt;
- font-variant: small-caps;
- }
- .project {
- border: 1px solid #303030;
- box-shadow: 0 0 2px #000000;
- padding: 5px 17px;
- margin: 7px 0;
- }
- .project > ul.nav {
- padding: 5px 7px;
- box-shadow: none;
- font-variant: normal;
- border: 0;
- border-top: 1px solid #303030;
- }
- .project > ul.nav > li {
- margin: 0 4px;
- }
- .project > ul.nav > li > a {
- color: #999999;
- cursor: pointer;
- }
- .project > ul.nav > li > a:hover {
- border: 0;
- }
- .form-group, .error {
- padding: 7px 17px;
- margin: 10px 0;
- border: 1px solid #191919;
- box-shadow: 0 0 2px #303030;
- }
- .error {
- color: #fd5a4b;
- }
- .form-group > label {
- display: block;
- padding: 0;
- margin: 0 0 5px 0;
- cursor: pointer;
- }
- input[type="text"], textarea {
- padding: 4px 7px;
- margin: 0;
- border: 1px solid #252525;
- background: #101010;
- width: 200px;
- color: #777777;
- }
- textarea {
- width: 400px;
- height: 250px;
- }
- input[type="button"] {
- padding: 7px 17px;
- margin: 0;
- border: 1px solid #303030;
- background: #181818;
- color: #777777;
- cursor: pointer;
- }
- table.builds {
- padding: 4px;
- margin: 20px 0;
- border: 1px solid #303030;
- color: #777777;
- width: 100%;
- box-shadow: inset 0 0 2px #000000;
- }
- table.builds > thead > tr > th {
- border: 1px solid #303030;
- text-align: left;
- padding: 2px 4px;
- font-weight: normal;
- font-variant: small-caps;
- }
- table.builds > tbody > tr > td {
- padding: 6px 4px;
- border-bottom: 1px solid #242424;
- }
- table.builds > tbody > tr > td.state {
- text-transform: capitalize;
- }
- table.builds > tbody > tr > td.success {
- color: #88bF8E;
- }
- table.builds > tbody > tr > td.failed {
- color: #fd5a4b;
- }
- table.builds > tbody > tr > td.running {
- color: #dfd270;
- }
- pre.log {
- border: 1px solid #303030;
- padding: 10px;
- }
Шаблоны
Шаблоны
templates/home.html -- Домашняя страница
- <div class="project" ng-repeat="project in projects">
- <a href="#project/{{project['id']}} ">{{project['name']}}</a>
- <dl>
- <dt>Description:</dt><dd>{{project['description']}}</dd>
- <dt>Repository:</dt><dd>{{project['url']}}</dd>
- </dl>
- </div>
templates/project.html -- Страница просмотра проекта
- <div class="project">
- <div class="title">{{project['name']}}</div>
- <dl>
- <dt>Description:</dt><dd>{{project['description']}}</dd>
- <dt>Repository:</dt><dd>{{project['url']}}</dd>
- </dl>
- <ul class="nav">
- <li><a ng-click="startBuild()">Build</a></li>
- <li><a href="#/edit-project/{{project['id']}}">Edit</a></li>
- <li><a href="#/remove-project/{{project['id']}}">Remove</a></li>
- </ul>
- </div>
- <table class="builds" ng-show="builds">
- <thead>
- <tr><th>№</th><th>Started</th><th>Finished</th><th>State</th></tr>
- </thead>
- <tbody>
- <tr ng-repeat="build in builds">
- <td><a href="#/build/{{project['id']}}/{{build['id']}}">#{{build['id']}}</a></td>
- <td>{{build['start_date']}}</td>
- <td>{{build['finish_date']}}</td>
- <td class="state {{build['state']}}">{{build['state']}}</td>
- </tr>
- </tbody>
- </table>
templates/build.html -- Страница просмотра сборки
- <div class="project">
- <div class="title"><a href="#/project/{{project['id']}}">{{project['name']}}</a></div>
- <dl>
- <dt>Description:</dt><dd>{{project['description']}}</dd>
- <dt>Repository:</dt><dd>{{project['url']}}</dd>
- </dl>
- </div>
- <table class="builds">
- <thead>
- <tr><th>№</th><th>Started</th><th>Finished</th><th>State</th></tr>
- </thead>
- <tbody>
- <tr>
- <td>#{{build['id']}}</td>
- <td>{{build['start_date']}}</td>
- <td>{{build['finish_date']}}</td>
- <td class="state {{build['state']}}">{{build['state']}}</td>
- </tr>
- </tbody>
- </table>
- <pre class="log" ng-show="build['log']">{{build['log']}}</pre>
templates/project-form.html -- Форма создания/редактирования проекта
- <div class="title">{{title}}</div>
- <div class="form-group">
- <label for="name">Name:</label>
- <input id="name" type="text" ng-model="project['name']" />
- </div>
- <div class="form-group">
- <label for="description">Description:</label>
- <textarea id="description" ng-model="project['description']"></textarea>
- </div>
- <div class="form-group">
- <label for="url">Repository URL:</label>
- <input id="url" type="text" ng-model="project['url']" />
- </div>
- <div class="error" ng-show="error">Error: {{error}}</div>
- <div class="form-group">
- <input type="button" ng-click="save()" value="Save" />
- </div>
templates/project-remove.html -- Форма удаления проекта
- <p>Do you really want to remove project?</p>
- <div>
- <input type="button" value="Remove" ng-click="confirm()" />
- <input type="button" value="Cancel" ng-click="cancel()" />
- </div>
templates/error.html -- Сообщение об ошибке
- <div>An error has occurred</div>
application.js
application.js
- (function () {
- angular.module(
- 'app',
- ['ngRoute', 'angular-websocket'], // Модули, которые должны быть загружены перед загрузкой нашего приложения
- function ($routeProvider) {
- // Определяем роуты
- $routeProvider
- .when('/', {templateUrl: '/templates/home.html', controller: 'home'})
- .when('/error', {templateUrl: '/templates/error.html'})
- .when('/project/:id', {templateUrl: '/templates/project.html', controller: 'project'})
- .when('/new-project', {templateUrl: '/templates/project-form.html', controller: 'newProject'})
- .when('/edit-project/:id', {templateUrl: '/templates/project-form.html', controller: 'editProject'})
- .when('/remove-project/:id', {templateUrl: '/templates/project-remove.html', controller: 'removeProject'})
- .when('/build/:pid/:bid', {templateUrl: '/templates/build.html', controller: 'showBuild'})
- .otherwise('/error') // Редирект, если обратились по несуществующему адресу
- }
- ).config(
- function(WebSocketProvider){
- WebSocketProvider.prefix('').uri('ws://buildserver/broadcast/'); // Устанавливаем урлу для вебсокетов
- }
- ).run(
- function ($rootScope, WebSocket) {
- $rootScope.$on("$routeChangeStart", function (event, next, prev) {
- if (prev && prev['$$route']['controller'] == 'showBuild') {
- WebSocket.send(JSON.stringify({action: 'unsubscribe'})); // Шлём сообщение о том, что больше не хотим следить за логом сборки
- }
- });
- }
- ).controller(
- 'home', // Домашняя страница
- function ($scope, $http) {
- $scope.projects = [];
- $http.get('/api/v1/projects').success(function (data) {
- $scope.projects = data['items'];
- });
- }
- ).controller(
- 'project', // Просмотр проекта
- function ($scope, $routeParams, $http, $location) {
- $scope.project = {};
- $scope.builds = {};
- $http.get('/api/v1/projects/' + $routeParams.id).success(
- function (data) {
- $scope.project = data['project'];
- $scope.builds = data['builds'];
- }
- ).error(
- function () {
- $location.path('/error')
- }
- );
- $scope.startBuild = function () {
- $http.post('/api/v1/projects/' + $routeParams.id + '/build', {}).success(
- function (data) {
- console.log('START BUILD:', data); // TODO: show notification
- }
- ).error(
- function (data) {
- console.log('START BUILD:', data); // TODO: show notification
- }
- );
- }
- }
- ).controller(
- 'newProject', // Создание проекта
- function ($scope, $http, $location) {
- $scope.title = 'New project';
- $scope.project = {'name': '', 'description': '', 'url': ''};
- $scope.error = '';
- $scope.save = function () {
- $http.post('/api/v1/projects', $scope.project).success(
- function (data) {
- $location.path('/project/' + data['id']);
- }
- ).error(
- function (data) {
- if (data.hasOwnProperty('message')) {
- $scope.error = data['message'];
- } else {
- $scope.error = 'An error has occurred';
- }
- }
- );
- }
- }
- ).controller(
- 'editProject', // Редактирование проекта
- function ($scope, $http, $location, $routeParams) {
- $scope.title = 'Edit project';
- $http.get('/api/v1/projects/' + $routeParams.id).success(
- function (data) {
- $scope.project = data['project'];
- $scope.save = function () {
- var params = {
- 'name': $scope.project['name'],
- 'description': $scope.project['description'],
- 'url': $scope.project['url']
- };
- $http.put('/api/v1/projects/' + $routeParams.id, params).success(
- function (data) {
- $location.path('/project/' + data['id']);
- }
- ).error(
- function (data) {
- if (data.hasOwnProperty('message')) {
- $scope.error = data['message'];
- } else {
- $scope.error = 'An error has occurred';
- }
- }
- );
- }
- }
- ).error(
- function () {
- $location.path('/error');
- }
- );
- }
- ).controller(
- 'removeProject', // Удаление проекта
- function ($scope, $http, $location, $routeParams) {
- $scope.confirm = function () {
- $http.delete('/api/v1/projects/' + $routeParams.id).success(
- function () {
- $location.path('/');
- }
- ).error(
- function () {
- $location.path('/error');
- }
- );
- };
- $scope.cancel = function () {
- $location.path('/project/' + $routeParams.id);
- }
- }
- ).controller(
- 'showBuild', // Просмотр лога сборки
- function ($scope, $http, $routeParams, $location, WebSocket) {
- $http.get('/api/v1/projects/' + $routeParams['pid'] + '/builds/' + $routeParams['bid']).success(
- function (data) {
- $scope.project = data['project'];
- $scope.build = data['build'];
- if ($scope.build['log'] == undefined) {
- $scope.build['log'] = '';
- }
- var subscribe = function () {
- // Шлём сообщение tornado, что хоти следить за логом
- WebSocket.send(JSON.stringify({action: 'subscribe', params: {build_id: $scope.build.id}}));
- };
- if (WebSocket.currentState() == 'OPEN') {
- // Если соединение открыто, то шлём сразу
- subscribe();
- } else {
- // Иначе ждём, пока соединение не будет открыто
- WebSocket.onopen(subscribe);
- }
- WebSocket.onmessage(function (event) {
- var msg = JSON.parse(event.data);
- if (msg['action'] == 'build_finished') {
- // Билд завершён, обновляем состояние в $scope, что отразится на шаблоне
- $scope.build['state'] = msg['params']['state']
- } else if (msg['action'] == 'build_log') {
- // Обновляем лог сборки
- $scope.build['log'] = $scope.build['log'] + '\n' + msg['params']['line'];
- }
- });
- }
- ).error(
- function () {
- $location.path('/error');
- }
- );
- }
- );
- })();
С фронтендом почти покончено.
Обновляем setup.py:
- from setuptools import setup, find_packages
- setup(
- name='buildserver',
- version='0.1',
- description='Simple buildserver',
- packages=find_packages('src'),
- package_dir={'': 'src'},
- install_requires=[
- 'flask',
- 'psycopg2',
- 'pyzmq',
- 'tornado'
- ],
- # Добавили точки входа
- entry_points={
- 'console_scripts': [
- 'broadcast=buildserver.broadcast:run',
- 'web=buildserver.web:run',
- 'worker=buildserver.worker:run'
- ]
- }
- )
Выполняем bin/buildout, после чего будут сгенерированы bin/broadcast, bin/web и bin/worker
Запускаем их.
Создаём тестовый проект:
$ mkdir /tmp/testrepo && cd /tmp/testrepo && git init
$ editor .buildserver
Пишем туда следующее:
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
- echo 'Step'
- sleep 1
$ git add .buildserver && git commit -m "Init"
Открываем в браузере http://buildserver добавляем проект. В качестве урлы указываем /tmp/testrepo.
После сохранения проекта жмём Build, обновляем страницу и переходим к просмотру лога.
Если я нигде не ошибся и вы всё сделали правильно, то всё должно быть ok.
Ну а теперь домашнее задание. Гг.
Сделать отображение уведомления о том, что билд был добавлен в очередь на сборку.
Обновлять список сборок в режиме реального времени (добавление/изменение/удаление).
Причём не ддосить базу запросами, а задействовать zmq и tornado.
На этом всё. Исходный код проекта можно найти здесь: https://github.com/Kilte/buildserver