chef_sous_vide 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/gem_tasks"
2
+ require "cucumber/rake/task"
3
+
4
+ Cucumber::Rake::Task.new
5
+
6
+ task :default => [ "cucumber" ]
7
+
8
+ begin
9
+ require "kitchen/rake_tasks"
10
+ Kitchen::RakeTasks.new
11
+ rescue LoadError
12
+ puts ">>>>> Kitchen gem not loaded, omitting tasks" unless ENV["CI"]
13
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sous_vide"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/kitchen.yml ADDED
@@ -0,0 +1,98 @@
1
+ ---
2
+ <% if (ARGV[0] == "converge") %>
3
+ transport:
4
+ name: rsync
5
+ <% end %>
6
+
7
+ driver:
8
+ name: docker
9
+ use_sudo: false
10
+ provision_command:
11
+ # prevent APT from deleting the APT folder
12
+ - rm /etc/apt/apt.conf.d/docker-clean
13
+ - apt-get install -y -q apt-transport-https rsync
14
+ # allow test-kitchen to use root user (connects via ssh)
15
+ - sed -i 's/prohibit-password/yes/' /etc/ssh/sshd_config
16
+ # disable systemd since it doesn't work in docker
17
+ - ln -sf /bin/true /bin/systemctl
18
+ # install chef-client as part of the image, save time in the runtime
19
+ - curl -LO https://omnitruck.chef.io/install.sh && bash ./install.sh -v 12.17.44 && rm install.sh
20
+ - ln /opt/chef/bin/chef-client /bin/chef-client
21
+ username: root
22
+ password: root
23
+
24
+ verifier:
25
+ sudo: false
26
+
27
+ provisioner:
28
+ install_strategy: 'skip'
29
+
30
+ platforms:
31
+ - name: ubuntu-16.04
32
+ - name: ubuntu-18.04
33
+
34
+ suites:
35
+ - name: default
36
+ includes:
37
+ - ubuntu-16.04
38
+ driver:
39
+ hostname: elasticsearch
40
+ instance_name: elasticsearch
41
+ forward:
42
+ - "5601:5601" # kibana
43
+ - "9200:9200" # es
44
+ run_list:
45
+ - sous_vide::install
46
+ - sous_vide::default
47
+ attributes:
48
+ kitchen:
49
+ roles:
50
+ - elasticsearch
51
+ java:
52
+ jdk_version: 8
53
+ elasticsearch:
54
+ install:
55
+ version: "6.6.2"
56
+ - name: tomcat
57
+ includes:
58
+ - ubuntu-18.04
59
+ - ubuntu-16.04
60
+ driver:
61
+ links:
62
+ - elasticsearch
63
+ run_list:
64
+ - sous_vide::install
65
+ - sous_vide::tomcat
66
+ attributes:
67
+ kitchen:
68
+ roles:
69
+ - tomcat-example
70
+ java:
71
+ jdk_version: 8
72
+ - name: nginx
73
+ includes:
74
+ - ubuntu-16.04
75
+ run_list:
76
+ - sous_vide::install
77
+ - sous_vide::nginx
78
+ attributes:
79
+ kitchen:
80
+ roles:
81
+ - nginx-example
82
+ nginx:
83
+ install_method: "source"
84
+ modules:
85
+ - nginx::headers_more_module
86
+ - nginx::http_auth_request_module
87
+ - nginx::http_echo_module
88
+ - nginx::http_geoip_module
89
+ - nginx::http_gzip_static_module
90
+ - nginx::http_realip_module
91
+ - nginx::http_v2_module
92
+ - nginx::http_ssl_module
93
+ - nginx::http_stub_status_module
94
+ - nginx::naxsi_module
95
+ - nginx::ngx_devel_module
96
+ - nginx::ngx_lua_module
97
+ - nginx::openssl_source
98
+ - nginx::upload_progress_module
data/lib/sous_vide.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "chef/handler"
2
+ require "chef/http"
3
+ require "sous_vide/version"
4
+
5
+ require "sous_vide/tracked_resource"
6
+ require "sous_vide/handler"
7
+
8
+ module SousVide
9
+ class Error < StandardError; end
10
+ end
@@ -0,0 +1,176 @@
1
+ module SousVide
2
+ # This module implements Chef event methods. It's based on Chef::EventDispatch::Base.
3
+ #
4
+ # Nested resources are explicitly ignored and the code flow will be as follows:
5
+ #
6
+ # 1. resource_action_start
7
+ # 2. resource_* events (possibly multiple)
8
+ # 3. resource_action_complete
9
+ #
10
+ # The code is intentionally procedural and explicit. If :resource_action_start assigned
11
+ # @processing_now only then other events will work. :resource_action_completed unassigns
12
+ # @processing_now so all events will be ignored until :resource_action_start is called
13
+ # again.
14
+ module EventMethods
15
+ # This hook will always fire whenever chef is about to converge a resource, including why_run
16
+ # mode, notifications or skipped resources.
17
+ def resource_action_start(new_resource, action, notification_type, notifying_resource)
18
+ if nested?(new_resource) # ignore nested resources
19
+ new_r_name = "#{new_resource.resource_name}[#{new_resource.name}]##{action}"
20
+ debug("Received :resource_action_start on #{new_r_name}.",
21
+ "It's a nested resource and will be ignored.")
22
+ return false
23
+ end
24
+
25
+ # This is a delayed notification. From now on we are in 'delayed' run phase.
26
+ if notification_type == :delayed && @run_phase != "delayed"
27
+ debug("Changed run phase to 'delayed'.")
28
+ @run_phase = "delayed"
29
+ end
30
+
31
+ @processing_now = create(chef_resource: new_resource, action: action)
32
+ debug("Received :resource_action_start on #{@processing_now}.")
33
+
34
+ @execution_order += 1
35
+ @processing_now.execution_order = @execution_order
36
+ @processing_now.execution_phase = @run_phase
37
+ @processing_now.started_at = Time.now.strftime("%F %T")
38
+
39
+ @processing_now.chef_resource_handle = new_resource
40
+
41
+ @processing_now.before_notifications = new_resource.before_notifications.size
42
+ @processing_now.immediate_notifications = new_resource.immediate_notifications.size
43
+ @processing_now.delayed_notifications = new_resource.delayed_notifications.size
44
+
45
+ # When notifying resource is present notification_type will also be present. It is nil for
46
+ # delayed notifications.
47
+ if notifying_resource
48
+ _name = "#{notifying_resource.resource_name}[#{notifying_resource.name}]"
49
+ debug("Notified from #{_name} (:#{notification_type})")
50
+ @processing_now.notifying_resource = _name
51
+ end
52
+ @processing_now.notification_type = notification_type
53
+ true
54
+ end
55
+
56
+ def resource_updated(new_resource, _action)
57
+ return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
58
+
59
+ debug("Received :resource_updated on #{@processing_now}")
60
+ @processing_now.status = "updated"
61
+ true
62
+ end
63
+
64
+ # Resource is skipped when a guard instruction stops the converge process or when
65
+ # `action :nothing` is used (it's a guard too).
66
+ def resource_skipped(new_resource, _action, conditional)
67
+ return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
68
+
69
+ debug("Received :resource_skipped on #{@processing_now}", "(#{conditional.to_text})")
70
+ @processing_now.guard_description = conditional.to_text
71
+ @processing_now.status = "skipped"
72
+ true
73
+ end
74
+
75
+ def resource_up_to_date(new_resource, _action)
76
+ return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
77
+
78
+ debug("Received :resource_up_to_date on #{@processing_now}")
79
+ @processing_now.status = "up-to-date"
80
+ true
81
+ end
82
+
83
+ def resource_completed(new_resource)
84
+ return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
85
+
86
+ debug("Received :resource_completed on #{@processing_now}")
87
+ @processing_now.duration_ms = (new_resource.elapsed_time.to_f * 1000).to_i
88
+
89
+ # If a resource has notifications Chef will converge it in a forced_why_run mode to
90
+ # determine if any update will happen and if the notifications should be called.
91
+ #
92
+ # When this event was fired in why_run mode we override it's status to `why-run`.
93
+ #
94
+ # This is also how Chef::Runner#focred_why_run is implemented so it should be reliable.
95
+ #
96
+ # TODO: what happens when :why_run for notification fails?
97
+ if ::Chef::Config[:why_run]
98
+ debug("Resource #{@processing_now.name}##{@processing_now.action} marked why-run",
99
+ "because Chef::Config[:why_run] is true.")
100
+ @processing_now.status = "why-run"
101
+ end
102
+
103
+ # This resource was not notified by another and is a subject of normal ordered converge
104
+ # process. @resource_collection_cursor is pointing to next resource according to the
105
+ # expanded resource collection.
106
+ #
107
+ # When chef-client fails we will take remaining entries and add to the report as
108
+ # 'unprocessed'. It works because resources are ordered and we can keep track where we are.
109
+ #
110
+ # why-run mode and notifications are not in natural order and must not move the cursor.
111
+ #
112
+ # Having it pointing ahead is relevant because current resource has just been converged
113
+ # (technically 'failed') and it is not 'unprocessed'.
114
+ if !@processing_now.notifying_resource && # not notified
115
+ !::Chef::Config[:why_run] && # not why-run
116
+ @run_phase == "converge" # only converge phase
117
+
118
+ @resource_collection_cursor += 1
119
+ end
120
+
121
+ @processing_now.completed_at = Time.now.strftime("%F %T")
122
+ @processed << @processing_now
123
+ @processing_now = nil
124
+ true
125
+ end
126
+
127
+ def resource_failed(new_resource, _action, exception)
128
+ return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
129
+
130
+ debug("Received :resource_failed on #{@processing_now}")
131
+ @processing_now.status = "failed"
132
+ @processing_now.error_source = new_resource.to_text
133
+ @processing_now.error_output = exception.message
134
+ true
135
+ end
136
+
137
+ # Resources with retries can succeed on subsequent attempts or ignore_failure option may be
138
+ # set and it's the only place we can capture intermittent errors.
139
+ #
140
+ # This event can fire multiple times, but we capture only the most recent error.
141
+ def resource_failed_retriable(new_resource, _action, _remaining_retries, exception)
142
+ return false if @processing_now.nil? || nested?(new_resource) # ignore nested resources
143
+
144
+ debug("Received :resource_failed_retriable on #{@processing_now}")
145
+ @processing_now.retries += 1
146
+ @processing_now.error_source = new_resource.to_text
147
+ @processing_now.error_output = exception.message
148
+ true
149
+ end
150
+
151
+ def converge_start
152
+ debug("Received :converge_start")
153
+ debug("Changed run phase to 'converge'.")
154
+ @run_phase = "converge"
155
+ @run_name ||= [@run_started_at, @chef_node_role, @chef_node_ipv4, @run_id].join(" ")
156
+ end
157
+
158
+ def converge_complete
159
+ debug("Received :converge_completed")
160
+ @run_success = true
161
+ @run_completed_at = Time.now.strftime("%F %T")
162
+ send_to_output!
163
+ end
164
+
165
+ def converge_failed(_exception)
166
+ debug("Received :converge_failed")
167
+ @run_success = false
168
+ @run_completed_at = Time.now.strftime("%F %T")
169
+ @run_phase = "post-converge"
170
+ debug("Changed run phase to 'post-converge'.")
171
+
172
+ consume_unprocessed_resources!
173
+ send_to_output!
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,186 @@
1
+ require "sous_vide/event_methods"
2
+ require "sous_vide/outputs/json_file"
3
+ require "sous_vide/outputs/json_http"
4
+ require "sous_vide/outputs/logger"
5
+ require "sous_vide/outputs/multi"
6
+
7
+ require "securerandom"
8
+ require "singleton"
9
+
10
+ module SousVide
11
+ # == SousVide::Handler
12
+ #
13
+ # The Handler receives event data from chef-client and keeps track of the converge process. It's
14
+ # essentially a stream parser hooked into Chef::EventDispatch.
15
+ #
16
+ # Event methods are all in SousVide::EventMethods module. This file contains logic that does not
17
+ # deal with events directly.
18
+ class Handler
19
+ include Singleton
20
+ include EventMethods
21
+
22
+ attr_accessor :chef_run_context,
23
+ :logger,
24
+ :sous_output,
25
+ :processing_now,
26
+ :processed,
27
+ :run_phase,
28
+ :run_id,
29
+ :run_name
30
+
31
+ # Enables the handler. Call it anywhere in the recipe, ideally as early as possible.
32
+ #
33
+ # SousVide::Handler.register(node.run_context)
34
+ #
35
+ # All converge-time resources will be included in the report regardless at what point
36
+ # registration happens.
37
+ #
38
+ # Compile-time resources defined before registration will not be included.
39
+ # TODO: see client.rb start_handlers as an option.
40
+ #
41
+ # `chef_handler` resource does not support subscribing to :events so we have give up DSL and
42
+ # use Chef API.
43
+ #
44
+ # The `Chef.event_handler` DSL could be used but dealing with returns and exceptions in procs
45
+ # is a pain.
46
+ def self.register(run_context)
47
+ ::Chef::Log.info "Registering SousVide"
48
+
49
+ instance.chef_run_context = run_context
50
+ instance.post_initialize
51
+
52
+ run_context.events.register(instance)
53
+ end
54
+
55
+ def initialize
56
+ @execution_order = 0
57
+ @resource_collection_cursor = 0
58
+
59
+ @processed = []
60
+ @processing_now = nil
61
+
62
+ @run_started_at = Time.now.strftime("%F %T")
63
+
64
+ # Not related to RunStatus#run_id, it's our internal run id.
65
+ @run_id = SecureRandom.uuid.split("-").first # => 596e9d00
66
+ @run_phase = "compile"
67
+
68
+ # Default to Chef logger, but can be changed to anything that responds to #call
69
+ @logger = ::Chef::Log
70
+ @sous_output = Outputs::Logger.new(logger: @logger)
71
+ end
72
+
73
+ # This is called in #register, as soon as @chef_run_context is available.
74
+ def post_initialize
75
+ @chef_node_ipv4 = @chef_run_context.node["ipaddress"] || "<no ip>"
76
+ @chef_node_role = @chef_run_context.node["roles"].first || "<no role>"
77
+ @chef_node_instance_id = @chef_run_context.node.name
78
+ end
79
+
80
+
81
+ def run_data
82
+ {
83
+ chef_run_id: @run_id,
84
+ chef_run_name: @run_name,
85
+ chef_run_started_at: @run_started_at,
86
+ chef_run_completed_at: @run_completed_at,
87
+ chef_run_success: @run_success
88
+ }
89
+ end
90
+
91
+ def node_data
92
+ {
93
+ chef_node_ipv4: @chef_node_ipv4,
94
+ chef_node_instance_id: @chef_node_instance_id,
95
+ chef_node_role: @chef_node_role
96
+ }
97
+ end
98
+
99
+ def create(chef_resource:, action:)
100
+ tracked = TrackedResource.new(action: action,
101
+ name: chef_resource.name,
102
+ type: chef_resource.resource_name)
103
+
104
+ tracked.cookbook_name = chef_resource.cookbook_name || "<Dynamically Defined Resource>"
105
+ tracked.cookbook_recipe = chef_resource.recipe_name || "<Dynamically Defined Resource>"
106
+ tracked.source_line = chef_resource.source_line || "<Dynamically Defined Resource>"
107
+ tracked.chef_resource_handle = chef_resource
108
+ tracked
109
+ end
110
+
111
+ # We will ignore nested resources. Once a top level resource triggered :resource_action
112
+ # started any events not related to it will be ignored.
113
+ def nested?(chef_resource)
114
+ @processing_now &&
115
+ @processing_now.chef_resource_handle != chef_resource
116
+ end
117
+
118
+ # When chef-client fails we haven't seen all resources and need to backfill the handler.
119
+ def consume_unprocessed_resources!
120
+ all_known_resources = expand_chef_resources!
121
+
122
+ # No unprocessed resources left. Failure likely occured on last resource or in a delayed
123
+ # notification.
124
+ # TODO: check delayed notification failure
125
+ return if @resource_collection_cursor >= all_known_resources.size
126
+
127
+ unprocessed = all_known_resources[@resource_collection_cursor..-1]
128
+
129
+ # We will pass unprocessed resources via :resource_action_start and :resource_completed so
130
+ # they will end up in @processed array, but with status set to 'unprocessed' and execution
131
+ # phase 'post-converge'.
132
+ #
133
+ # TODO: consider placing these resources before delayed notification, currently they are
134
+ # always at the very end. It _maybe_ makes sense.
135
+ unprocessed.each do |tracked|
136
+ resource_action_start(
137
+ tracked.chef_resource_handle, # new_resource
138
+ tracked.action, # action
139
+ nil, # notification_type
140
+ nil # notifying_resource
141
+ )
142
+
143
+ resource_completed(
144
+ tracked.chef_resource_handle # new_resource
145
+ )
146
+ end
147
+ end
148
+
149
+ # Resources with multiple actions must be expanded, ie. given resource:
150
+ #
151
+ # service 'nginx' do
152
+ # action [:enable, :start]
153
+ # end
154
+ #
155
+ # After expansion we should have 2 resources:
156
+ #
157
+ # * service[nginx] with action :enable
158
+ # * service[nginx] with action :start
159
+ #
160
+ # We keep track of the progress and on failure we will pick up unprocessed resources from here
161
+ # to feed the handler in post-converge stage.
162
+ #
163
+ # On a successful chef-client run @processed will contain all resources and this method won't
164
+ # be called.
165
+ def expand_chef_resources!
166
+ chef_run_context.resource_collection.flat_map do |chef_resource|
167
+ Array(chef_resource.action).map do |action|
168
+ create(chef_resource: chef_resource, action: action)
169
+ end
170
+ end
171
+ end
172
+
173
+ def send_to_output!
174
+ @sous_output.call(run_data: run_data, node_data: node_data, resources_data: @processed)
175
+ end
176
+
177
+ def debug(*args)
178
+ message = args.compact.join(" ")
179
+ logger.debug(message)
180
+ end
181
+
182
+ def logger
183
+ @logger ||= ::Chef::Log
184
+ end
185
+ end
186
+ end