Node.js feature-flipping through Git
Git as a Continuous Manager
@m4d_zhttps://talks.m4dz.net/git-ci/en/My last time with that was a month ago with GitHub
if ( ff.flag('crazy') ) {
doCrazyExperiment()
} else {
doReallySafeStuff()
}
Feature Flag vs. Branching
// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// allow function accepts arrays as any parameter
acl.allow('member', 'blogs', ['edit', 'view', 'delete'])
// test jsmith permissions
acl.isAllowed('jsmith', 'blogs', ['edit', 'view', 'delete'])
// check james permissions
acl.allowedPermissions('james', ['blogs', 'forums'], (err, perms) => {
console.log(perms)
})
// run ACL as a middleware
app.put('/blogs/:id', acl.middleware(), (req, res, next) => { /*...*/ })
module.exports = {
up () {
return new Promise((resolve, reject) => { /*...*/ })
},
down () {
return new Promise((resolve, reject) => { /*...*/ })
}
}
$ npm run db:migrate [up|down] {version}
_('click', $('#cats-btn'))
.throttle(500) // can be fired once in every 500ms
.pipe(getDataFromServer)
.map(formatCats)
.pipe(UI.render);
Server side
$ ssh admin@staging
|admin@staging| $ git init --bare /srv/git/my-project.git
|admin@staging| $ tree -L 2 /srv/git/my-project.git
/srv/git/my-project.git
|-- branches
|-- hooks
| |-- post-checkout
| |-- post-commit
| |-- post-merge
| |-- post-receive
| |-- post-receive.d
| |-- pre-push
| |-- update
| `-- update.d
|-- index
|-- info
`-- refs
|-- heads
`-- tags
On developers side
[~my-project] $ git add remote staging git@staging:/srv/git/my-project.git
#!/bin/sh
# $GIT_BARE_REPOSITORY/hooks/post-receive
GIT_DIR=$(dirname $PWD)
while read oldrev newrev ref
do
# Load tasks from hooks/post-receive.d/*
if test -d "$PWD/post-receive.d"
then
for task in "$PWD/post-receive.d/*.sh"
do
test -r $task && source $task
done
unset task
fi
done
Filter
#!/bin/sh
# $GIT_BARE_REPOSITORY/hooks/update
while read oldrev newrev ref
do
if [[ $ref = refs/heads/production ]]
then
# Commands to run on `production` branch only
fi
done
Deploy
#!/bin/sh
# $GIT_BARE_REPOSITORY/hooks/post-receive.d/10-deploy
$RUN_DIR="/srv/my-app"
if [[ $ref = refs/heads/production ]]
then
git --work-tree=$RUN_DIR \
--git-dir=$GIT_DIR \
checkout --force production
fi
npm scripts
#!/bin/sh
# $GIT_BARE_REPOSITORY/hooks/post-receive.d/20-npm-run-migrate
$RUN_DIR="/srv/my-app"
if [[ $ref = refs/heads/production ]]
then
cd $RUN_DIR && npm run db:migrate up 2018-nodejs-it
fi
Git Push!
[~/my-project] $ git push staging production
How to inform the workers about the changes?
Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new “features”.
Doug McIlroy, in Bell System Technical Journal, 1978, 1th rule
Unix-socket:
echo "SIGNAL db:migrated" | /usr/bin/socat - UNIX-CONNECT:/var/run/my-app.sock
Use a Node.js instance as coordinator
const net = require('net')
const socket = net.createServer(sock => {
sock.write(`Hello ${sock.remoteAddress}\n`)
// when `socat` writes a message
sock.on('data', data => {
if (data === 'SIGNAL db:migrated') {
//...
}
})
})
socket.listen('/var/run/my-app.sock')
Use streams for FRP
How to broadcast/dispatch the actions?
Self-reload Workers
const {fork} = require('child_process')
(function main () {
const server = fork('server.js').unref()
sock.connect('tcp://127.0.0.1:3000')
sock.on('RESTART', () => {
server.kill()
setTimeout(main, 1000)
})
})()
1/ Init ACL/Flags
const flags = ff.config({
criteria () { return [
{ id: 'userInGroup',
check (user, group) { acl.isAllowed(user.id, group, '*') }},
{ id: 'environment',
check (user, env) { return process.env.NODE_ENV === env }}
]},
features () { return [
{ id: 'admin',
criteria: { userInGroup: 'admin' }},
{ id: 'development',
criteria: { environment: 'development' }}
]}
})
2/ Reload ACL/Flags
sock.connect('tcp://127.0.0.1:3000')
sock.on('ACL:RELOAD', ({perms, features}) => {
controls.removeAllow('*', '*', '*')
controls.allow(perms)
flags.config({features})
flags.reload()
})
Enable API endpoints on-the-fly
// hook to initialize the endpoint route at runtime
app.post('/api/:endpoint', (req, res) => {
if (req.params.endpoint === 'list') {
if (flags.isFeatureEnabledForUser('list', user)) {
let listController = require('controllers/ListController')
listController.init(app)
res.status(200).send()
} else {
res.status(401).send()
}
}
})
app.get('/api/:endpoint', (req, res) => { /*...*/})
Use HTTP Codes correctly
204
: No Content303
: See Other307
: Temporary Redirect401
: Unauthorized403
: Forbidden1/ REST API
2/ Update JWT
3/ Block/Redirect requests
WebSocket
Push API !
const subscription
const payload
const options = {
TTL: 3600
}
webPush.sendNotification(subscription, payload, options)
.then(() => res.sendStatus(201))
.catch(err => res.sendStatus(500))
Service Worker/Notification
self.addEventListener('push', event => {
const payload = event.data ? event.data.text() : 'no payload'
event.waitUntil(
// Reload ACL/flags
self.registration.showNotification('ACL Updated!', {
body: payload
})
)
})
Code Splitting
const getComponent = () => {
return flags.isFeatureEnabledForUser('component', user)
? import(/* webpackChunkName: "lodash" */ 'lodash')
.then(({ default: _ }) => {
let element = document.createElement('div')
element.innerHTML = _.join(['Hello', 'world'], ' ')
return element
})
.catch(error => 'An error occurred while loading the component')
: Promise.reject(new Error(403))
}
getComponent().then(component => { /*...*/ })
Simple Monitoring View
DevOps are here to ensure your users to not suffer a mean time in your continuous delivery. And Node.js, with Streams and FRP, is a great tool for DevOps tasks.
Paranoïd Web Dino · Tech Evangelist
https://talks.m4dz.net/git-ci/en/ Available under licence CC BY-SA 4.0
m4dz, CC BY-SA 4.0
Courtesy of Unsplash and Pexels contributors
Powered by Reveal.js
Source code available at
https://git.madslab.net/talks