chef_sous_vide 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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