├── .gitignore ├── .kitchen.yml ├── .rubocop.yml ├── Berksfile ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Thorfile ├── Vagrantfile ├── attributes └── default.rb ├── chefignore ├── libraries └── canaria.rb ├── metadata.rb ├── recipes └── default.rb └── test ├── cookbooks └── canaria-test │ ├── .kitchen.yml │ ├── metadata.rb │ └── recipes │ └── default.rb ├── environments └── canary.json └── unit └── canaria_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | .#* 4 | \#*# 5 | .*.sw[a-z] 6 | *.un~ 7 | pkg/ 8 | 9 | # Berkshelf 10 | .vagrant 11 | /cookbooks 12 | Berksfile.lock 13 | 14 | # Bundler 15 | Gemfile.lock 16 | bin/* 17 | .bundle/* 18 | 19 | .kitchen/ 20 | .kitchen.local.yml 21 | -------------------------------------------------------------------------------- /.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | 5 | provisioner: 6 | name: chef_zero 7 | environments_path: './test/environments' 8 | 9 | platforms: 10 | - name: ubuntu-12.04 11 | - name: centos-6.4 12 | 13 | suites: 14 | - name: zero 15 | run_list: 16 | - canaria-test::default 17 | attributes: 18 | canaria: 19 | percentage: 0 20 | - name: twentyfive 21 | run_list: 22 | - canaria-test::default 23 | attributes: 24 | canaria: 25 | percentage: 25 26 | - name: fifty 27 | run_list: 28 | - canaria-test::default 29 | attributes: 30 | canaria: 31 | percentage: 50 32 | - name: onehundred 33 | run_list: 34 | - canaria-test::default 35 | attributes: 36 | canaria: 37 | percentage: 100 38 | - name: default 39 | run_list: 40 | - canaria-test::default 41 | attributes: 42 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AlignParameters: 2 | Enabled: false 3 | ClassLength: 4 | Enabled: false 5 | MethodLength: 6 | Enabled: false 7 | PerceivedComplexity: 8 | Enabled: false 9 | CyclomaticComplexity: 10 | Enabled: false 11 | Metrics/AbcSize: 12 | Enabled: false 13 | -------------------------------------------------------------------------------- /Berksfile: -------------------------------------------------------------------------------- 1 | source "https://supermarket.chef.io" 2 | 3 | cookbook 'canaria-test', path: 'test/cookbooks/canaria-test' 4 | 5 | metadata 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | Initial release of canaria 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'berkshelf' 4 | 5 | # Uncomment these lines if you want to live on the Edge: 6 | # 7 | # group :development do 8 | # gem "berkshelf", github: "berkshelf/berkshelf" 9 | # gem "vagrant", github: "mitchellh/vagrant", tag: "v1.6.3" 10 | # end 11 | # 12 | # group :plugins do 13 | # gem "vagrant-berkshelf", github: "berkshelf/vagrant-berkshelf" 14 | # gem "vagrant-omnibus", github: "schisamo/vagrant-omnibus" 15 | # end 16 | 17 | gem 'thor-foodcritic' 18 | gem 'test-kitchen' 19 | gem 'kitchen-vagrant' 20 | 21 | group :development do 22 | gem 'chef' 23 | gem 'rspec' 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Ryan Cragun 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # canaria-cookbook 2 | 3 | A library cookbook to extend the Chef DSL to include `canary?` and 4 | `set_chef_environment` helpers. When properly configured it will allow nodes to 5 | autonomously determine whether they're a canary and to take appropriate actions. 6 | In my case it was developed to allow nodes to change environments for rolling 7 | upgrades, however, it could be used to guard for any canary operations you'd like. 8 | Most of the time it will be within 5% of the configured canary percentage, though 9 | it does vary depending on your node count and hostname patterns. If you 10 | require 100% accuracy for canary nodes there is an option to whitelist them via 11 | node FQDN. 12 | 13 | ## Controlling the canaries 14 | 15 | The idea here is to allow nodes to autonomously decide if they are canaries, 16 | however, there are several ways in which you can control which nodes 17 | are canaries: hostname overrides, canary percentage, a combination of both. 18 | 19 | If you want to specify nodes you can set the overrides manually. If you need an 20 | exact percentage of nodes you can do a knife search, sort, map to set the hostname 21 | overrides. 22 | 23 | ## How the the DSL helpers work 24 | `canary?` works by hashing the node FQDN and does a modulo over 100 to determine 25 | which out of 100 groups the node belongs to. If nodes group is between 26 | 0 and the configured percentage it will be a canary. 27 | 28 | Unlike `node.chef_environment`, `set_chef_environment` will verify that the 29 | environment exists and raise an error if an invalid environment is used. 30 | 31 | ## How to use the canaria cookbook 32 | Include `canaria` in your node's `run_list`: 33 | 34 | ```json 35 | { 36 | "run_list": [ 37 | "recipe[canaria::default]" 38 | ] 39 | } 40 | ``` 41 | 42 | Configure the the percentage and overrides in your environments 43 | 44 | ```ruby 45 | # environments/my_app_canary.rb 46 | override_attributes( 47 | 'canaria' => { 48 | 'overrides' => ['host.my_org.com'], 49 | 'percentage' => 0 50 | } 51 | ) 52 | ``` 53 | 54 | Use the helpers in your applications recipe 55 | 56 | ```ruby 57 | 58 | if canary? 59 | # Do canary things like change into the canary environment 60 | set_chef_environment(node['my_app']['canary']['canary_environment']) 61 | 62 | # Or maybe install the canary version of your application if you don't have 63 | # a separate environment 64 | my_app do 65 | version 'canary' 66 | action :install 67 | end 68 | else 69 | # Ensure we're in prod 70 | set_chef_environment(node['my_app']['canary']['prod_environment']) 71 | 72 | # Or install the stable version of the package if you don't use multiple 73 | # environments 74 | my_app do 75 | version 'stable' 76 | action :install 77 | end 78 | end 79 | ``` 80 | 81 | ## Rolling canary environments explained 82 | After the pipeline has gone green in the Development, Rehearsal and Union 83 | and it's time to promote to Production, we'll first want to test our changes on 84 | a few select canary nodes before doing a rolling upgrade out to 10%, 50% and 85 | finally 100% percent of our applications nodes. Because our attributes 86 | and cookbook versions are pinned via Chef environment, we'll control the rollout 87 | by promoting changes to our applications canary and production environments. 88 | 89 | ### Pipeline promotion steps 90 | * Change the canary percentage attribute in our applications canary 91 | and production environments. Changing the value in both environments will ensure 92 | that all canaries will stay in the canary environment. 93 | 94 | ```ruby 95 | # environments/my_app_production.rb 96 | cookbook_versions( 97 | "my_app"=>"~> 1.2.0" 98 | ) 99 | override_attributes( 100 | 'canaria' => { 101 | 'overrides' => ['host.my_org.com'], 102 | 'percentage' => 10 103 | } 104 | ) 105 | ``` 106 | ```ruby 107 | # environments/my_app_canary.rb 108 | cookbook_versions( 109 | "my_app"=>"~> 1.3.0" 110 | ) 111 | override_attributes( 112 | 'canaria' => { 113 | 'overrides' => ['host.my_org.com'], 114 | 'percentage' => 100 115 | } 116 | ) 117 | ``` 118 | 119 | * Wait for the nodes to converge and upgrade. You can determine a safe grace 120 | period by summing the converge frequency, converge splay and average increase in converge length during upgrades. 121 | 122 | * Increase canary percentage in the prod environment. 123 | ```ruby 124 | # environments/my_app_production.rb 125 | cookbook_versions( 126 | "my_app"=>"~> 1.2.0" 127 | ) 128 | override_attributes( 129 | 'canaria' => { 130 | 'overrides' => ['host.my_org.com'], 131 | 'percentage' => 20 132 | } 133 | ) 134 | ``` 135 | ```ruby 136 | # environments/my_app_canary.rb 137 | cookbook_versions( 138 | "my_app"=>"~> 1.3.0" 139 | ) 140 | override_attributes( 141 | 'canaria' => { 142 | 'overrides' => ['host.my_org.com'], 143 | 'percentage' => 100 144 | } 145 | ) 146 | ``` 147 | 148 | * Wait for the nodes to converge and upgrade 149 | 150 | * Repeat the increase and wait steps until it is time to push to 100% of nodes. 151 | 152 | * Promote our application Canary environment to Production and change the canary percentage to zero in both of our environments 153 | 154 | ```ruby 155 | # environments/my_app_production.rb 156 | cookbook_versions( 157 | "my_app"=>"~> 1.3.0" 158 | ) 159 | override_attributes( 160 | 'canaria' => { 161 | 'overrides' => [], 162 | 'percentage' => 0 163 | } 164 | ) 165 | ``` 166 | ```ruby 167 | # environments/my_app_canary.rb 168 | cookbook_versions( 169 | "my_app"=>"~> 1.3.0" 170 | ) 171 | override_attributes( 172 | 'canaria' => { 173 | 'overrides' => [], 174 | 'percentage' => 0 175 | } 176 | ) 177 | ``` 178 | 179 | After the final step all nodes should eventually upgrade and join the production 180 | environment. In the event of a rollback, all we have to do is change the canary percentage to zero in both environments. 181 | 182 | ## Attributes 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |
KeyTypeDescriptionDefault
['canaria']['percentage']IntegerWhat percentage of nodes should be selected as canaries0
['canaria']['overrides']ArrayAn array of node FQDNs that will automatically be canaries[]
204 | 205 | ## License and Authors 206 | 207 | Author:: Ryan Cragun () 208 | -------------------------------------------------------------------------------- /Thorfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'bundler' 4 | require 'bundler/setup' 5 | require 'thor/foodcritic' 6 | require 'berkshelf/thor' 7 | 8 | begin 9 | require "kitchen/thor_tasks" 10 | Kitchen::ThorTasks.new 11 | rescue LoadError 12 | puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"] 13 | end 14 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = '2' 6 | 7 | Vagrant.require_version '>= 1.5.0' 8 | 9 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 10 | # All Vagrant configuration is done here. The most common configuration 11 | # options are documented and commented below. For a complete reference, 12 | # please see the online documentation at vagrantup.com. 13 | 14 | config.vm.hostname = 'canaria-berkshelf' 15 | 16 | # Set the version of chef to install using the vagrant-omnibus plugin 17 | # NOTE: You will need to install the vagrant-omnibus plugin: 18 | # 19 | # $ vagrant plugin install vagrant-omnibus 20 | # 21 | if Vagrant.has_plugin?("vagrant-omnibus") 22 | config.omnibus.chef_version = 'latest' 23 | end 24 | 25 | # Every Vagrant virtual environment requires a box to build off of. 26 | # If this value is a shorthand to a box in Vagrant Cloud then 27 | # config.vm.box_url doesn't need to be specified. 28 | config.vm.box = 'chef/ubuntu-14.04' 29 | 30 | 31 | # Assign this VM to a host-only network IP, allowing you to access it 32 | # via the IP. Host-only networks can talk to the host machine as well as 33 | # any other machines on the same network, but cannot be accessed (through this 34 | # network interface) by any external networks. 35 | config.vm.network :private_network, type: 'dhcp' 36 | 37 | # Create a forwarded port mapping which allows access to a specific port 38 | # within the machine from a port on the host machine. In the example below, 39 | # accessing "localhost:8080" will access port 80 on the guest machine. 40 | 41 | # Share an additional folder to the guest VM. The first argument is 42 | # the path on the host to the actual folder. The second argument is 43 | # the path on the guest to mount the folder. And the optional third 44 | # argument is a set of non-required options. 45 | # config.vm.synced_folder "../data", "/vagrant_data" 46 | 47 | # Provider-specific configuration so you can fine-tune various 48 | # backing providers for Vagrant. These expose provider-specific options. 49 | # Example for VirtualBox: 50 | # 51 | # config.vm.provider :virtualbox do |vb| 52 | # # Don't boot with headless mode 53 | # vb.gui = true 54 | # 55 | # # Use VBoxManage to customize the VM. For example to change memory: 56 | # vb.customize ["modifyvm", :id, "--memory", "1024"] 57 | # end 58 | # 59 | # View the documentation for the provider you're using for more 60 | # information on available options. 61 | 62 | # The path to the Berksfile to use with Vagrant Berkshelf 63 | # config.berkshelf.berksfile_path = "./Berksfile" 64 | 65 | # Enabling the Berkshelf plugin. To enable this globally, add this configuration 66 | # option to your ~/.vagrant.d/Vagrantfile file 67 | config.berkshelf.enabled = true 68 | 69 | # An array of symbols representing groups of cookbook described in the Vagrantfile 70 | # to exclusively install and copy to Vagrant's shelf. 71 | # config.berkshelf.only = [] 72 | 73 | # An array of symbols representing groups of cookbook described in the Vagrantfile 74 | # to skip installing and copying to Vagrant's shelf. 75 | # config.berkshelf.except = [] 76 | 77 | config.vm.provision :chef_solo do |chef| 78 | chef.json = { 79 | mysql: { 80 | server_root_password: 'rootpass', 81 | server_debian_password: 'debpass', 82 | server_repl_password: 'replpass' 83 | } 84 | } 85 | 86 | chef.run_list = [ 87 | 'recipe[canaria::default]' 88 | ] 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /attributes/default.rb: -------------------------------------------------------------------------------- 1 | default['canaria']['percentage'] = 0 2 | default['canaria']['overrides'] = [] 3 | -------------------------------------------------------------------------------- /chefignore: -------------------------------------------------------------------------------- 1 | # Put files/directories that should be ignored in this file when uploading 2 | # or sharing to the community site. 3 | # Lines that start with '# ' are comments. 4 | 5 | # OS generated files # 6 | ###################### 7 | .DS_Store 8 | Icon? 9 | nohup.out 10 | ehthumbs.db 11 | Thumbs.db 12 | 13 | # SASS # 14 | ######## 15 | .sass-cache 16 | 17 | # EDITORS # 18 | ########### 19 | \#* 20 | .#* 21 | *~ 22 | *.sw[a-z] 23 | *.bak 24 | REVISION 25 | TAGS* 26 | tmtags 27 | *_flymake.* 28 | *_flymake 29 | *.tmproj 30 | .project 31 | .settings 32 | mkmf.log 33 | 34 | ## COMPILED ## 35 | ############## 36 | a.out 37 | *.o 38 | *.pyc 39 | *.so 40 | *.com 41 | *.class 42 | *.dll 43 | *.exe 44 | */rdoc/ 45 | 46 | # Testing # 47 | ########### 48 | .watchr 49 | .rspec 50 | spec/* 51 | spec/fixtures/* 52 | test/* 53 | features/* 54 | Guardfile 55 | Procfile 56 | 57 | # SCM # 58 | ####### 59 | .git 60 | */.git 61 | .gitignore 62 | .gitmodules 63 | .gitconfig 64 | .gitattributes 65 | .svn 66 | */.bzr/* 67 | */.hg/* 68 | */.svn/* 69 | 70 | # Berkshelf # 71 | ############# 72 | cookbooks/* 73 | tmp 74 | 75 | # Cookbooks # 76 | ############# 77 | CONTRIBUTING 78 | CHANGELOG* 79 | 80 | # Strainer # 81 | ############ 82 | Colanderfile 83 | Strainerfile 84 | .colander 85 | .strainer 86 | 87 | # Vagrant # 88 | ########### 89 | .vagrant 90 | Vagrantfile 91 | 92 | # Travis # 93 | ########## 94 | .travis.yml 95 | -------------------------------------------------------------------------------- /libraries/canaria.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | 3 | # Determine if a node is a canary 4 | module Canaria 5 | def self.canary?(unique_string, percent, overrides = []) 6 | return true if overrides.include?(unique_string) 7 | return false if percent.to_i == 0 8 | Digest::MD5.hexdigest(unique_string).to_s.hex % 100 <= percent.to_i 9 | end 10 | 11 | def self.chef_environment(node, chef_env) 12 | begin 13 | Chef::Environment.load(chef_env) 14 | rescue Net::HTTPServerException => e 15 | msg = 'Chef Environment error: ' 16 | if e.response.code.to_s == '404' 17 | msg << "#{chef_env} does not exist, cannot change." 18 | else 19 | msg << "#{chef_env} raised #{e.message}" 20 | end 21 | Chef::Log.error(msg) 22 | raise 23 | end 24 | 25 | node.chef_environment(chef_env) 26 | end 27 | 28 | # DSL module we'll mix into the Chef DSL 29 | module DSL 30 | def canary? 31 | Canaria.canary?(node['fqdn'], 32 | node['canaria']['percentage'], 33 | node['canaria']['overrides']) 34 | end 35 | 36 | # rubocop:disable AccessorMethodName 37 | def set_chef_environment(chef_env) 38 | Canaria.chef_environment(node, chef_env) 39 | end 40 | # rubocop:enable AccessorMethodName 41 | end 42 | end 43 | 44 | if defined?(Chef) 45 | Chef::Recipe.send(:include, Canaria::DSL) 46 | Chef::Provider.send(:include, Canaria::DSL) 47 | Chef::Resource.send(:include, Canaria::DSL) 48 | Chef::ResourceDefinition.send(:include, Canaria::DSL) 49 | end 50 | -------------------------------------------------------------------------------- /metadata.rb: -------------------------------------------------------------------------------- 1 | name 'canaria' 2 | maintainer 'Ryan Cragun' 3 | maintainer_email 'ryan@chef.io' 4 | license 'Apache 2.0' 5 | description 'Add a canary? method to the Chef DSL' 6 | long_description 'Add a canary? method to the Chef DSL' 7 | version '0.2.0' 8 | -------------------------------------------------------------------------------- /recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: canaria 3 | # Recipe:: default 4 | # 5 | # Copyright (C) 2015 Ryan Cragun 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | -------------------------------------------------------------------------------- /test/cookbooks/canaria-test/.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | 5 | provisioner: 6 | name: chef_solo 7 | 8 | platforms: 9 | - name: ubuntu-12.04 10 | - name: centos-6.4 11 | 12 | suites: 13 | - name: default 14 | run_list: 15 | attributes: 16 | -------------------------------------------------------------------------------- /test/cookbooks/canaria-test/metadata.rb: -------------------------------------------------------------------------------- 1 | name 'canaria-test' 2 | maintainer 'Ryan Cragun' 3 | maintainer_email 'ryan@chef.io' 4 | license 'Apache 2.0' 5 | description 'Installs/Configures canaria-test' 6 | long_description 'Installs/Configures canaria-test' 7 | version '0.1.0' 8 | 9 | depends 'canaria' 10 | -------------------------------------------------------------------------------- /test/cookbooks/canaria-test/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: canaria-test 3 | # Recipe:: default 4 | # 5 | # Copyright (C) 2015 Ryan Cragun 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | set_chef_environment 'canary' if canary? 20 | 21 | log "#{node.chef_environment}" 22 | -------------------------------------------------------------------------------- /test/environments/canary.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "canary", 3 | "description": "The development environment", 4 | "cookbook_versions": { 5 | }, 6 | "json_class": "Chef::Environment", 7 | "chef_type": "environment", 8 | "default_attributes": { 9 | }, 10 | "override_attributes": { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/unit/canaria_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative File.expand_path('libraries/canaria') 2 | require 'rspec' 3 | require 'chef' 4 | 5 | describe Canaria do 6 | describe '.canary?' do 7 | # 1000 fake random nodes 8 | let(:nodes) do 9 | @nodes ||= begin 10 | nodes = [] 11 | 250.times do 12 | %w(west east dublin singapore).each do |region| 13 | nodes << "node-#{Random.new(1000)}-#{region}.test.com" 14 | end 15 | end 16 | nodes 17 | end 18 | end 19 | 20 | (1..99).each do |percent| 21 | context "When the Canary % is set to #{percent}" do 22 | it "successfully determines canaries withing 5% of #{percent}" do 23 | canary_count = nodes.inject(0) do |count, node| 24 | described_class.canary?(node, percent) ? count + 1 : count 25 | end 26 | 27 | expect(canary_count).to be_within(50).of(percent * 10) 28 | end 29 | end 30 | end 31 | 32 | [0, 100].each do |percent| 33 | context "When the Canary % is set to #{percent}" do 34 | it 'successfully determines the exact number of canaries' do 35 | canary_count = nodes.inject(0) do |count, node| 36 | described_class.canary?(node, percent) ? count + 1 : count 37 | end 38 | 39 | expect(canary_count).to eq(percent * 10) 40 | end 41 | end 42 | end 43 | 44 | context 'when overrides are given' do 45 | let(:node) { 'node-01-dublin.test.com' } 46 | let(:overrides) do 47 | nodes = [] 48 | (1..9).each do |i| 49 | nodes << "node-0#{i}-dublin.test.com" 50 | end 51 | nodes 52 | end 53 | 54 | context 'when a node is in the overrides' do 55 | context 'when the percent is zero' do 56 | it 'correctly determines that it is a canary' do 57 | expect(described_class.canary?(node, 0, overrides)).to eq(true) 58 | end 59 | end 60 | 61 | context 'when the percent is non-zero' do 62 | it 'correctly determines that it is a canary' do 63 | expect(described_class.canary?(node, 5, overrides)).to eq(true) 64 | end 65 | end 66 | end 67 | 68 | context 'when a node is not the overrides' do 69 | let(:node) { 'node-01-singapore.test.com' } 70 | 71 | context 'when the percent is zero' do 72 | it 'correctly determines that it is a canary' do 73 | expect(described_class.canary?(node, 0, overrides)).to eq(false) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | 80 | describe 'chef_environment' do 81 | let(:node) { instance_double('Node', chef_environment: true) } 82 | let(:new_env) { 'canary' } 83 | 84 | context 'when the environment exists' do 85 | before do 86 | allow(Chef::Environment) 87 | .to receive(:load) 88 | .with(new_env) 89 | .and_return(true) 90 | end 91 | 92 | it 'sets the environment on the node' do 93 | expect(node).to receive(:chef_environment).with(new_env).once 94 | Canaria.chef_environment(node, new_env) 95 | end 96 | end 97 | 98 | context 'when the environment does not exist' do 99 | let(:exception) do 100 | response = Net::HTTPResponse.new('1.1', '404', 'ON NO') 101 | Net::HTTPServerException.new('OH NO', response) 102 | end 103 | 104 | let(:msg) do 105 | "Chef Environment error: #{new_env} does not exist, cannot change." 106 | end 107 | 108 | before do 109 | allow(Chef::Environment) 110 | .to receive(:load) 111 | .with(new_env) 112 | .and_raise(exception) 113 | 114 | allow(Chef::Log).to receive(:error).with(msg) 115 | end 116 | 117 | it 'logs the error' do 118 | expect(Chef::Log).to receive(:error).with(msg) 119 | expect { Canaria.chef_environment(node, new_env) }.to raise_error 120 | end 121 | 122 | it 'does not attempt to change the environment' do 123 | expect(node).to_not receive(:anything) 124 | expect { Canaria.chef_environment(node, new_env) }.to raise_error 125 | end 126 | end 127 | end 128 | end 129 | --------------------------------------------------------------------------------