This commit is contained in:
Sebastian Frank 2019-03-05 15:28:29 +01:00
commit a3cce2df73
7 changed files with 443 additions and 0 deletions

31
Controller/Admin.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace Drone\Controller;
use Cockpit\AuthController;
/**
* Admin controller class.
*/
class Admin extends AuthController
{
/**
* Default index controller.
*/
public function index()
{
if (!$this->app->module('cockpit')->hasaccess('drone', 'manage.view')) {
return false;
}
$data = $this->app->module('drone')->fetchDeploys();
return $this->render('drone:views/deploys/index.php', [
'deploys' => $data['deploys'] ?? [],
'building' => $data['building'] ?? FALSE,
'build' => $data['build']
]);
}
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Paulo Gomes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# Cockpit CMS Drone Deploys Addon
This addons is a modified version of the addon [CockpitCMS-Netlify](https://github.com/pauloamgomes/CockpitCMS-Netlify).
It provides an integration with Drone CI promote feature to trigger a pipeline run. You can use it to deploy a website based on data in Cockpit CMS and built with a static site generator.
## Installation
1. Confirm that you have Cockpit CMS (Next branch) installed and working.
2. Download and extract to 'your-cockpit-docroot/addons' (e.g. cockpitcms/addons/Drone, the addon folder name must be Drone)
3. Drone icon will apear, if configuration is completed.
## Configuration
1. Ensure that from your Drone account you have an access token and permissions to promote a build for the project and branch, you want to use.
2. Edit Cockpit config/config.yaml and add a new entry for drone like below:
```yaml
drone:
url: https://drone.yourserver.de
token: <your drone access token>
owner: <the owner of the git repository>
project: <the git project>
branch: <the branch in your git repository for the deployment>
environment: <the environment used in your drone pipeline>
build: <the build number used as base for deployment> # leave empty and the addon will use the latest successfull build based on a push event
```
### Permissions
There are just two permissions:
- **manage.view** - provides access to the Drone deploy list
- **manage.deploy** - provides access to trigger a new deploy
## Usage
Having the configuration defined accessing the Drone deploys page (/drone/deploys) a list of latest (limited to 50) deploys is displayed:
To trigger a new deploy just hist the Deploy button an confirm the action.
## Copyright and license
Copyright 2019 Sebastian Frank under the MIT license.

29
admin.php Normal file
View File

@ -0,0 +1,29 @@
<?php
/**
* @file
* Addon admin functions.
*/
// Module ACL definitions.
$this("acl")->addResource('drone', [
'manage.view',
'manage.deploy',
]);
$app->on('admin.init', function () use ($app) {
if ($app->config['drone']) {
// Bind admin routes.
$this->bindClass('Drone\\Controller\\Admin', 'drone/deploys');
if ($app->module('cockpit')->hasaccess('drone', 'deploys.view')) {
// Add to modules menu.
$this('admin')->addMenuItem('modules', [
'label' => 'Drone Deploys',
'icon' => 'drone:icon.svg',
'route' => '/drone/deploys',
'active' => strpos($this['route'], '/drone/deploys') === 0,
]);
}
}
});

133
bootstrap.php Normal file
View File

@ -0,0 +1,133 @@
<?php
/**
* @file
* Implements bootstrap functions.
*/
// Include addon functions only if its an admin request.
if (COCKPIT_ADMIN && !COCKPIT_API_REQUEST) {
// Extend addon functions.
$this->module('drone')->extend([
'fetchDeploys' => function ($limit = 50) {
$settings = $this->app->config['drone'] ?? FALSE;
if (!$settings || !isset($settings['url'],
$settings['owner'],
$settings['project'],
$settings['environment'],
$settings['token'])) {
return [];
}
// $url = $settings['api_url'] . '/sites/' . $settings['site_id'] . '/deploys' . '?access_token=' . $settings['access_token'];
$url = trim($settings['url'], "/") . '/api/repos/' . $settings['owner'] . '/' . $settings['project'] . '/builds';
$branch = $settings['branch'] ? $settings['branch'] : 'master';
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'Authorization: Bearer ' . $settings['token']
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$deploys = [];
$unfiltered_deploys = curl_exec($ch);
curl_close($ch);
$unfiltered_deploys = json_decode($unfiltered_deploys);
$build = $settings['build'];
if ($unfiltered_deploys && is_array($unfiltered_deploys)) {
foreach($unfiltered_deploys as $deploy) {
if ($deploy->{target} == $branch && ($deploy->event == 'push' || ($deploy->event == 'promote' && $deploy->deploy_to == $settings['environment']))) {
// find latest successful build
if (!$build) {
if ($deploy->event == 'push' && $deploy->status == 'success') {
$build = $deploy->number;
} else {
$build = $deploy->parent;
}
}
$limit--;
if ($limit>0) {
$deploys[] = $deploy;
}
}
}
}
// Parse dates and check if any deploy is on building status.
$building = false;
foreach ($deploys as $idx => $deploy) {
$deploys[$idx]->building = false;
if (!in_array($deploy->status, ['success', 'failure'])) {
$building = true;
$deploys[$idx]->building = true;
}
$deploys[$idx]->created_at = date('Y-m-d H:i', $deploy->created);
$deploys[$idx]->updated_at = date('Y-m-d H:i', $deploy->updated);
if ($deploy->finished) {
$deploys[$idx]->deploy_time = $deploy->finished - $deploy->started;
}
}
return [
'deploys' => $deploys,
'building' => $building,
'build' => $build
];
},
'createDeploy' => function ($fromBuild) {
$settings = $this->app->config['drone'];
if (!$fromBuild || !$settings || !isset($settings['url'],
$settings['owner'],
$settings['project'],
$settings['environment'],
$settings['token'])) {
return array(
"error" => "missing settings"
);
}
$url = trim($settings['url'], "/") . '/api/repos/' . $settings['owner'] . '/' . $settings['project'] . '/builds/' . $fromBuild . '/promote?target=' . $settings['environment'];
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'Authorization: Bearer ' . $settings['token']
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($result);
if ($httpcode >= 400) {
$decoded->error = "ERROR: $httpcode";
}
return $decoded;
},
]);
// Include admin.
include_once __DIR__ . '/admin.php';
}

8
icon.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="218px" viewBox="0 0 256 218" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g fill="#252728">
<path d="M128.224307,0.72249586 C32.0994301,0.72249586 0.394430682,84.5663333 0.394430682,115.221578 L78.3225537,115.221578 C78.3225537,115.221578 89.3644231,75.2760497 128.224307,75.2760497 C167.08419,75.2760497 178.130047,115.221578 178.130047,115.221578 L255.605569,115.221578 C255.605569,84.5623457 224.348186,0.72249586 128.224307,0.72249586"></path>
<path d="M227.043854,135.175898 L178.130047,135.175898 C178.130047,135.175898 169.579477,175.122423 128.224307,175.122423 C86.8691361,175.122423 78.3225537,135.175898 78.3225537,135.175898 L30.2571247,135.175898 C30.2571247,145.426215 67.9845088,217.884246 128.699837,217.884246 C189.414168,217.884246 227.043854,158.280482 227.043854,135.175898"></path>
<circle cx="128" cy="126.076531" r="32.7678394"></circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

175
views/deploys/index.php Normal file
View File

@ -0,0 +1,175 @@
<style>
.uk-modal-details .uk-modal-dialog {
height: 85%;
}
</style>
<div>
<ul class="uk-breadcrumb">
<li class="uk-active"><span>@lang('Drone Deploys')</span></li>
</ul>
</div>
<div class="uk-margin-top" riot-view>
@if($app->module('cockpit')->hasaccess('drone', 'manage.view'))
<div class="uk-form uk-clearfix" show="{!loading}">
@if($app->module('cockpit')->hasaccess('drone', 'manage.deploy'))
<div class="uk-float-right">
<a class="uk-button uk-button-primary uk-button-large" onclick="{createDeploy}">
<i class="uk-icon-plus uk-icon-justify"></i> @lang('Deploy')
</a>
</div>
@endif
</div>
<div class="uk-text-xlarge uk-text-center uk-text-primary uk-margin-large-top" show="{ loading }">
<i class="uk-icon-spinner uk-icon-spin"></i>
</div>
<div class="uk-text-large uk-text-center uk-margin-large-top uk-text-muted" show="{ !loading && !deploys.length }">
<img class="uk-svg-adjust" src="@url('drone:icon.svg')" width="100" height="100" alt="@lang('Drone Deploys')" data-uk-svg />
<p>@lang('No deploys found')</p>
</div>
<div class="uk-modal uk-modal-details uk-height-viewport">
<div class="uk-modal-dialog uk-modal-dialog-large">
<a href="" class="uk-modal-close uk-close"></a>
<h3>{ deploy && deploy.title }</h3>
<div class="uk-margin uk-flex uk-flex-middle" if="{deploy}">
<codemirror ref="codemirror" syntax="json"></codemirror>
</div>
</div>
</div>
<div class="uk-form uk-clearfix" show="{!loading}">
<table class="uk-table uk-table-tabbed uk-table-striped uk-margin-top" if="{ !loading && deploys.length }">
<thead>
<tr>
<th class="uk-text-small uk-link-muted uk-noselect" width="70">
@lang('State')
</th>
<th class="uk-text-small uk-link-muted uk-noselect" width="50">
@lang('Build') #
</th>
<th class="uk-text-small uk-link-muted uk-noselect" width="450">
@lang('Title')
</th>
<th class="uk-text-small uk-link-muted uk-noselect" width="120">
@lang('Created')
</th>
<th class="uk-text-small uk-link-muted uk-noselect" width="120">
@lang('Updated')
</th>
<th class="uk-text-small uk-link-muted uk-noselect" width="90">
@lang('Deploy time')
</th>
</tr>
</thead>
<tbody>
<tr each="{deploy, $index in deploys}" class="{ deploy.state == 'error' ? 'uk-text-danger' : ''}">
<td>
<a onclick="{ showdeployDetails }" class="extrafields-indicator uk-text-nowrap">
<span class="uk-badge uk-text-small" if="{!deploy.building && deploy.status !== 'failure' && deploy.status !== 'success' }"><i class="uk-icon-eye uk-icon-justify"></i>{ App.i18n.get(deploy.status) }</span>
<span class="uk-badge uk-text-small uk-badge-success" if="{deploy.status === 'success'}"><i class="uk-icon-eye uk-icon-justify"></i>{ App.i18n.get(deploy.status) }</span>
<span class="uk-badge uk-text-small uk-badge-danger" if="{deploy.status === 'failure'}"><i class="uk-icon-eye uk-icon-justify"></i>{ App.i18n.get(deploy.status) }</span>
<span class="uk-badge uk-text-small uk-badge-warning" if="{deploy.building}"><i class="uk-icon-spinner uk-icon-spin"></i>{ App.i18n.get(deploy.status) }</span>
</a>
</td>
<td>{ deploy.number }</td>
<td>{ deploy.event }: { deploy.message }</td>
<td><span class="uk-badge uk-badge-outline uk-text-muted">{ deploy.created_at }</span></td>
<td><span class="uk-badge uk-badge-outline uk-text-muted">{ deploy.updated_at }</span></td>
<td><span if="{deploy.deploy_time}">{ deploy.deploy_time }s</span></td>
</tr>
</tbody>
</table>
</div>
@endif
<script type="view/script">
var $this = this;
$this.deploy = {};
$this.loading = true;
$this.deploys = {{ json_encode($deploys) }};
$this.building = {{ json_encode($building) }};
$this.build = {{ json_encode($build) }};
this.on('mount', function() {
$this.loading = false;
$this.modal = UIkit.modal(App.$('.uk-modal-details', this.root), {modal:true});
if ($this.building) {
setTimeout(function() {
$this.fetchData();
}, 5000);
}
$this.update();
});
showdeployDetails(e) {
$this.deploy = e.item.deploy;
$this.modal.show();
editor = $this.refs.codemirror.editor;
editor.setValue(JSON.stringify($this.deploy, null, 2), true);
editor.setOption("readOnly", true);
editor.setSize($this.modal.dialog[0].clientWidth - 50, $this.modal.dialog[0].clientHeight - 70);
editor.refresh();
$this.trigger('ready');
}
createDeploy() {
if ($this.building) {
App.ui.notify(App.i18n.get("A deploy is already in progress, please wait until finishes."), "warning");
} else {
UIkit.modal.confirm(App.i18n.get("Triggering a new Drone deploy. Are you sure?<br><br>Using build: #") + $this.build, function() {
App.callmodule('drone:createDeploy', $this.build).then(function(data) {
if (!data || !data.result || data.result.error) {
if (data && data.result && data.result.error) {
App.ui.notify(App.i18n.get(data.result.error), "danger");
} else {
App.ui.notify("unknown error", "danger");
}
if (data) {
$this.modal.show();
editor = $this.refs.codemirror.editor;
editor.setValue(JSON.stringify(data, null, 2), true);
editor.setOption("readOnly", true);
editor.setSize($this.modal.dialog[0].clientWidth - 50, $this.modal.dialog[0].clientHeight - 70);
editor.refresh();
}
return;
}
App.ui.notify(App.i18n.get("A new deploy was requested."), "success");
setTimeout(function() {
App.ui.notify(App.i18n.get("Fetching deploy status..."), "success");
$this.building = true;
$this.fetchData();
}, 2000)
});
});
}
}
fetchData() {
if (!this.building) {
return;
}
App.callmodule('drone:fetchDeploys').then(function(data) {
if (data && data.result && data.result.deploys) {
$this.deploys = data.result.deploys;
$this.building = data.result.building;
$this.build = data.result.build;
setTimeout(function() {
$this.fetchData();
}, 5000);
} else {
App.ui.notify(App.i18n.get("Cannot fetch deploys from Drone! Try again later."), "danger");
}
$this.update();
});
}
</script>
</div>