Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callbacks to deployment events, refresh #176

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ language: ruby
before_install:
- gem update
- gem install bundler
rvm:
- 1.9.3
- 2.0.0
- 2.1.0
- 2.2.0
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,30 @@ You have to set the following keys:

Modify the paths as appropriate for your cert, ca, and key files.

### Callbacks

You can create callbacks to perform custom actions during a deploy.

```ruby
task :production => :common do
before_stopping_container do |server, service|
my_loadbalancer.disable server.hostname
end

before_starting_container do |server, service|
my_chat_server.post "#{server.hostname} starting #{service.image}..."
end

after_starting_container do |server, service|
my_chat_server.post "#{server.hostname} started #{service.image}, waiting for health check..."
end

after_health_check_ok do |server, service|
my_loadbalancer.enable server.hostname
end
end
```

Deploying
---------

Expand Down
4 changes: 4 additions & 0 deletions lib/centurion/deploy.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
require 'excon'
require 'socket'

require_relative 'deploy_callbacks'

module Centurion; end

module Centurion::Deploy

FAILED_CONTAINER_VALIDATION = 100


def stop_containers(target_server, service, timeout = 30)
old_containers = if service.public_ports.nil? || service.public_ports.empty? || service.network_mode == 'host'
info "Looking for containers with names like #{service.name}"
Expand Down
41 changes: 41 additions & 0 deletions lib/centurion/deploy_callbacks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module Centurion
# Callbacks to allow hooking into the deploy lifecycle. This could
# be useful to communicate with a loadbalancer, chat room, etc.
module DeployCallbacks
def stop_containers(server, service, timeout = 30)
emit :before_stopping_container, server, service
super server, service, timeout
end

def before_starting_container(server, service)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain this actually gets called.

emit :before_starting_container, server, service
end

def start_new_container(server, service, restart_policy)
super(server, service, restart_policy).tap { emit :after_starting_container, server, service }
end

def wait_for_health_check_ok(health_check_method, server, port, endpoint, image_id, tag, sleep_time=5, retries=12)
super(health_check_method,
server,
port,
endpoint,
image_id,
tag,
sleep_time,
retries).tap { emit :after_health_check_ok, server }
end

private

def emit(name, *args)
callbacks[name].each do |callback|
callback.call(*args)
end
end

def callbacks
fetch 'callbacks', Hash.new { [] }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this fails with:

NoMethodError: undefined method `fetch' for Centurion::DeployCallbacks:Module
  /Users/intjonathan/sites/centurion/lib/centurion/deploy_callbacks.rb:38:in `callbacks'
  /Users/intjonathan/sites/centurion/lib/centurion/deploy_callbacks.rb:32:in `emit'
  /Users/intjonathan/sites/centurion/lib/centurion/deploy_callbacks.rb:11:in `before_starting_container'
  /Users/intjonathan/sites/centurion/lib/tasks/deploy.rake:143:in `block (3 levels) in <top (required)>'
  /Users/intjonathan/sites/centurion/lib/centurion/deploy_dsl.rb:8:in `call'
  /Users/intjonathan/sites/centurion/lib/centurion/deploy_dsl.rb:8:in `block (2 levels) in on_each_docker_host'

I'm not sure how it was supposed to work given the history - @relistan any ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the idea had been to keep fetch and other env methods out of that code entirely and to have the required data passed in instead. I'd need to spend some more time looking at this code to see what the right thing is to do there.

end
end
end
39 changes: 39 additions & 0 deletions lib/centurion/deploy_dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,47 @@ def defined_restart_policy
Centurion::Service::RestartPolicy.new(fetch(:restart_policy_name, 'on-failure'), fetch(:restart_policy_max_retry_count, 10))
end

def before_stopping_container(callback = nil, &block)
on :before_stopping_container, callback, &block
end

def before_starting_container(callback = nil, &block)
on :before_starting_container, callback, &block
end

def after_container_started(callback = nil, &block)
on :after_container_started, callback, &block
end

def after_health_check_ok(callback = nil, &block)
on :after_health_check_ok, callback, &block
end

def on(name, callback = nil, &block)
abort('A callback or block is required') unless callback || block
abort('Callback expects a lambda, proc, or block') if callback && !callback.respond_to?(:call)
callbacks[name] <<= (callback || block)
end

private

def callbacks
fetch('callbacks') || set('callbacks', Hash.new { [] })
end

def service_under_construction
service = fetch(:service,
Centurion::Service.from_hash(
fetch(:project),
image: fetch(:image),
hostname: fetch(:container_hostname),
dns: fetch(:custom_dns)
)
)
set(:service, service)
end


def build_server_group
hosts, docker_path = fetch(:hosts, []), fetch(:docker_path)
Centurion::DockerServerGroup.new(hosts, docker_path, build_tls_params)
Expand Down
3 changes: 3 additions & 0 deletions lib/tasks/deploy.rake
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ end

namespace :deploy do
include Centurion::Deploy
include Centurion::DeployCallbacks

namespace :dogestry do
task :validate_pull_image do
Expand Down Expand Up @@ -139,6 +140,8 @@ namespace :deploy do

stop_containers(server, service, fetch(:stop_timeout, 30))

Centurion::DeployCallbacks.before_starting_container(server, service)

container = start_new_container(server, service, defined_restart_policy)

public_ports = service.public_ports - fetch(:rolling_deploy_skip_ports, [])
Expand Down
104 changes: 104 additions & 0 deletions spec/deploy_callbacks_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
require 'spec_helper'
require 'centurion'

RSpec.describe Centurion::DeployCallbacks do
shared_examples_for 'a callback' do
let(:server) { double :server }
let(:service) { double :service }

let(:klass) do
Class.new do
include Centurion::DeployCallbacks
def method_missing(method_name, *_args)
doing method_name
end
end
end

let(:object) do
klass.new
end

before do
allow(object).to receive(:emit)
end
end

shared_examples_for 'the before_stopping_container callbacks' do |callback, method_name|
include_examples 'a callback'

it 'invokes all the callbacks' do
expect(object).to receive(:emit).with(callback, server, service).ordered
expect(object).to receive(:doing).with(method_name).ordered
subject
end
end

shared_examples_for 'the before_starting_container callbacks' do |callback|
include_examples 'a callback'

it 'invokes all the callbacks' do
expect(object).to receive(:emit).with(callback, server, service).ordered
subject
end
end

shared_examples_for 'the after_starting_container callbacks' do |callback, method_name|
include_examples 'a callback'

it 'invokes all the callbacks' do
expect(object).to receive(:doing).with(method_name).ordered
expect(object).to receive(:emit).with(callback, server, service).ordered
subject
end
end

shared_examples_for 'the after_health_check_ok callbacks' do |callback, method_name|
include_examples 'a callback'

it 'invokes all the callbacks' do
expect(object).to receive(:doing).with(method_name).ordered
expect(object).to receive(:emit).with(callback, server).ordered
subject
end
end

describe 'the before_stopping_container callback' do
subject { object.stop_containers server, service }
it_behaves_like 'the before_stopping_container callbacks',
:before_stopping_container,
:stop_containers
end

describe 'before_starting_container callback' do
subject { object.before_starting_container server, service }
it_behaves_like 'the before_starting_container callbacks',
:before_starting_container
end

describe 'after started callback' do
subject { object.start_new_container server, service, double }
it_behaves_like 'the after_starting_container callbacks',
:after_starting_container,
:start_new_container
end

describe 'after health check ok callback' do
let(:args) do
[
double(:health_check_method),
server,
double(:port),
double(:endpoint),
double(:image_id),
double(:tag),
double(:sleep),
double(:retries)
]
end
subject { object.wait_for_health_check_ok(*args) }
it_behaves_like 'the after_health_check_ok callbacks',
:after_health_check_ok,
:wait_for_health_check_ok
end
end
27 changes: 27 additions & 0 deletions spec/deploy_dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,31 @@ class DeployDSLTest
DeployDSLTest.set(:image, 'charlemagne')
expect(DeployDSLTest.defined_service.image).to eq('charlemagne:roland')
end

describe 'callbacks' do
shared_examples_for 'a callback for' do |callback_name|
it 'accepts procs' do
callback = ->(_) {}
allow(DeployDSLTest).to receive(:on)
expect(DeployDSLTest).to receive(:on).with(callback_name, callback)
DeployDSLTest.send callback_name, callback
end
end

describe '#before_stopping_container' do
it_behaves_like 'a callback for', :before_stopping_container
end

describe '#before_starting_container' do
it_behaves_like 'a callback for', :before_starting_container
end

describe '#after_container_started' do
it_behaves_like 'a callback for', :after_container_started
end

describe '#after_health_check_ok' do
it_behaves_like 'a callback for', :after_health_check_ok
end
end
end