├── .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 | Key |
187 | Type |
188 | Description |
189 | Default |
190 |
191 |
192 | ['canaria']['percentage'] |
193 | Integer |
194 | What percentage of nodes should be selected as canaries |
195 | 0 |
196 |
197 |
198 | ['canaria']['overrides'] |
199 | Array |
200 | An array of node FQDNs that will automatically be canaries |
201 | [] |
202 |
203 |
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 |
--------------------------------------------------------------------------------