Skip to content

Instantly share code, notes, and snippets.

@thomascube
Created January 4, 2020 13:53
Show Gist options
  • Save thomascube/77702323bd2852d7c28f801d077a20a6 to your computer and use it in GitHub Desktop.
Save thomascube/77702323bd2852d7c28f801d077a20a6 to your computer and use it in GitHub Desktop.
Automated Docker builds with Google cloud functions
const bent = require('bent');
const httpJSON = bent('json');
const admin = require('firebase-admin');
const DOCKER_BUILD_TRIGGER = 'https://hub.docker.com/api/build/v1/source/<this-is-secret>/trigger/<another-secret>/call/';
const BUILD_TAGS = {
'1.3.x-apache': '7.2-apache',
'1.3.x-fpm': '7.2-fpm',
'1.3.x-fpm-alpine': '7.2-fpm-alpine',
'1.4.x-apache': '7.3-apache',
'1.4.x-fpm': '7.3-fpm',
'1.4.x-fpm-alpine': '7.3-fpm-alpine',
'latest-apache': '7.3-apache',
'latest-fpm': '7.3-fpm',
'latest-fpm-alpine': '7.3-fpm-alpine',
};
let database;
let imageVersionCache = {};
/**
* Modify the given tag name into a valid database key
*
* @param {String} tag
* @return {String}
*/
function getDBKey(tag) {
return tag.replace(/[.#$]/g, 'x');
}
/**
* Fetch PHP docker image info for the given tag
*
* @param {String} tag
* @return {Object}
*/
async function fetchImageVersion(tag) {
// return cached result
if (imageVersionCache[tag]) {
return imageVersionCache[tag];
}
// console.log('DO fetchImageVersion', tag);
let data = await httpJSON('https://hub.docker.com/v2/repositories/library/php/tags/?page_size=25&page=1&name=' + encodeURIComponent(tag));
let result = null;
// console.log('DONE fetchImageVersion', tag, data.results);
if (Array.isArray(data.results)) {
data.results.forEach((item) => {
if (item.name === tag) {
result = item;
}
});
imageVersionCache[tag] = result;
}
return result;
}
/**
* Fetch the build version stored from last check/build
*
* @param {String} tag
* @return {String}
*/
async function fetchBuildVersion(tag) {
// read from databse
return database.ref('build-version/' + getDBKey(tag)).once('value')
.then((data) => {
// console.log('DONE fetchBuildVersion', tag, data.val())
return data.val() ? data.val().version : null;
});
}
/**
* Trigger a new Roundcube docker build for the given tag
*
* @param {String} tag
*/
async function triggerBuild(tag) {
// console.log('DO Trigger Build for ' + tag);
const httpPost = bent('POST', 'json', 202);
const response = await httpPost(DOCKER_BUILD_TRIGGER, {docker_tag: tag});
// console.log('DONE Trigger Build', response);
return response.state === 'Success' || response.state === 'Building';
}
/**
* Write the last build version into the database
*
* @param {String} tag
* @param {String} version
*/
async function updateBuildVersion(tag, version) {
// console.log('DO updateBuildVersion', tag, version);
return database.ref('build-version/' + getDBKey(tag)).set({version: version});
}
/**
* Main routine to check for new PHP image versions
*
* This triggers a new Roundcube build if the specific PHP image
* has been updated since the last build.
*
* @param {String} phpTag
* @param {String} rcubeTag
* @param {Boolean} dryRun
*/
async function checkPhpImage(phpTag, rcubeTag, dryRun) {
const [latest, build] = await Promise.all([fetchImageVersion(phpTag), fetchBuildVersion(phpTag)]);
if (latest && latest.last_updated && (!build || build < latest.last_updated)) {
if (!dryRun) {
try {
await triggerBuild(rcubeTag);
await updateBuildVersion(phpTag, latest.last_updated);
} catch (err) {
console.error('Failed to checkPhpImage(' + BUILD_TAGS[rcubeTag] + ', ' + rcubeTag + ')', err);
return '! New build failed for ' + rcubeTag;
}
}
return '* New build triggered for ' + rcubeTag + ' from php:' + phpTag + (dryRun ? ' (dry-run)' : '');
}
return '* No update for php:' + phpTag + ' (' + rcubeTag + ')';
}
// main entry point for runtime
module.exports.handler = async (event) => {
database = admin.database();
imageVersionCache = {}; // clear in-memory cache
let tasks = [];
let dryRun = event.query && Boolean(event.query.dryrun);
for (let rcubeTag in BUILD_TAGS) {
tasks.push(checkPhpImage(BUILD_TAGS[rcubeTag], rcubeTag, dryRun));
}
let buildLog = await Promise.all(tasks);
return {log: buildLog.join('\n')};
};
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const app = require('./app');
admin.initializeApp();
// register http endpoint
exports.dockerSync = functions.https.onRequest(async (request, response) => {
let res = await app.handler(request)
var msg = 'SUCCESS!\n';
if (res.log) {
msg += res.log + '\n';
}
response.send(msg);
});
// register scheduled function
exports.scheduledFunctionCrontab = functions.pubsub.schedule('0 9 * * *').timeZone('UTC').onRun(async (context) => {
console.log('Start scheduled execution');
let res = await app.handler(context);
if (res.log) {
console.log(res.log);
} else {
console.log('Done');
}
return null;
});
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"lint": "eslint .",
"serve": "firebase serve --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
"dependencies": {
"bent": "^7.0.2",
"firebase-admin": "^8.6.0",
"firebase-functions": "^3.3.0"
},
"devDependencies": {
"eslint": "^5.12.0",
"eslint-plugin-promise": "^4.0.1",
"firebase-functions-test": "^0.1.6"
},
"private": true
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment