diff --git a/.travis.yml b/.travis.yml index 04bd7cb9..5500f4e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index 3ddf831d..eb7d9206 100644 --- a/README.md +++ b/README.md @@ -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 --------- diff --git a/lib/centurion/deploy.rb b/lib/centurion/deploy.rb index 74b8c450..6f824fba 100644 --- a/lib/centurion/deploy.rb +++ b/lib/centurion/deploy.rb @@ -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}" diff --git a/lib/centurion/deploy_callbacks.rb b/lib/centurion/deploy_callbacks.rb new file mode 100644 index 00000000..b82ed7e3 --- /dev/null +++ b/lib/centurion/deploy_callbacks.rb @@ -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) + 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 { [] } + end + end +end diff --git a/lib/centurion/deploy_dsl.rb b/lib/centurion/deploy_dsl.rb index 726778cf..5dd25890 100644 --- a/lib/centurion/deploy_dsl.rb +++ b/lib/centurion/deploy_dsl.rb @@ -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) diff --git a/lib/tasks/deploy.rake b/lib/tasks/deploy.rake index 3598dae8..dde84e18 100644 --- a/lib/tasks/deploy.rake +++ b/lib/tasks/deploy.rake @@ -47,6 +47,7 @@ end namespace :deploy do include Centurion::Deploy + include Centurion::DeployCallbacks namespace :dogestry do task :validate_pull_image do @@ -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, []) diff --git a/spec/deploy_callbacks_spec.rb b/spec/deploy_callbacks_spec.rb new file mode 100644 index 00000000..3febfc0d --- /dev/null +++ b/spec/deploy_callbacks_spec.rb @@ -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 diff --git a/spec/deploy_dsl_spec.rb b/spec/deploy_dsl_spec.rb index cc6c3cbb..338492eb 100644 --- a/spec/deploy_dsl_spec.rb +++ b/spec/deploy_dsl_spec.rb @@ -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