clc-cheffish 0.8.clc

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +4 -0
  4. data/Rakefile +23 -0
  5. data/lib/chef/provider/chef_acl.rb +434 -0
  6. data/lib/chef/provider/chef_client.rb +48 -0
  7. data/lib/chef/provider/chef_container.rb +50 -0
  8. data/lib/chef/provider/chef_data_bag.rb +50 -0
  9. data/lib/chef/provider/chef_data_bag_item.rb +273 -0
  10. data/lib/chef/provider/chef_environment.rb +78 -0
  11. data/lib/chef/provider/chef_group.rb +78 -0
  12. data/lib/chef/provider/chef_mirror.rb +138 -0
  13. data/lib/chef/provider/chef_node.rb +82 -0
  14. data/lib/chef/provider/chef_organization.rb +150 -0
  15. data/lib/chef/provider/chef_resolved_cookbooks.rb +41 -0
  16. data/lib/chef/provider/chef_role.rb +79 -0
  17. data/lib/chef/provider/chef_user.rb +53 -0
  18. data/lib/chef/provider/private_key.rb +219 -0
  19. data/lib/chef/provider/public_key.rb +82 -0
  20. data/lib/chef/resource/chef_acl.rb +65 -0
  21. data/lib/chef/resource/chef_client.rb +44 -0
  22. data/lib/chef/resource/chef_container.rb +18 -0
  23. data/lib/chef/resource/chef_data_bag.rb +18 -0
  24. data/lib/chef/resource/chef_data_bag_item.rb +114 -0
  25. data/lib/chef/resource/chef_environment.rb +71 -0
  26. data/lib/chef/resource/chef_group.rb +49 -0
  27. data/lib/chef/resource/chef_mirror.rb +47 -0
  28. data/lib/chef/resource/chef_node.rb +18 -0
  29. data/lib/chef/resource/chef_organization.rb +64 -0
  30. data/lib/chef/resource/chef_resolved_cookbooks.rb +31 -0
  31. data/lib/chef/resource/chef_role.rb +104 -0
  32. data/lib/chef/resource/chef_user.rb +51 -0
  33. data/lib/chef/resource/private_key.rb +44 -0
  34. data/lib/chef/resource/public_key.rb +21 -0
  35. data/lib/cheffish.rb +222 -0
  36. data/lib/cheffish/actor_provider_base.rb +131 -0
  37. data/lib/cheffish/basic_chef_client.rb +115 -0
  38. data/lib/cheffish/chef_provider_base.rb +231 -0
  39. data/lib/cheffish/chef_run_data.rb +19 -0
  40. data/lib/cheffish/chef_run_listener.rb +28 -0
  41. data/lib/cheffish/key_formatter.rb +109 -0
  42. data/lib/cheffish/merged_config.rb +94 -0
  43. data/lib/cheffish/recipe_dsl.rb +147 -0
  44. data/lib/cheffish/server_api.rb +52 -0
  45. data/lib/cheffish/version.rb +3 -0
  46. data/lib/cheffish/with_pattern.rb +21 -0
  47. data/spec/functional/fingerprint_spec.rb +64 -0
  48. data/spec/functional/merged_config_spec.rb +20 -0
  49. data/spec/integration/chef_acl_spec.rb +914 -0
  50. data/spec/integration/chef_client_spec.rb +110 -0
  51. data/spec/integration/chef_container_spec.rb +34 -0
  52. data/spec/integration/chef_group_spec.rb +324 -0
  53. data/spec/integration/chef_mirror_spec.rb +244 -0
  54. data/spec/integration/chef_node_spec.rb +211 -0
  55. data/spec/integration/chef_organization_spec.rb +244 -0
  56. data/spec/integration/chef_user_spec.rb +90 -0
  57. data/spec/integration/private_key_spec.rb +446 -0
  58. data/spec/integration/recipe_dsl_spec.rb +29 -0
  59. data/spec/support/key_support.rb +29 -0
  60. data/spec/support/repository_support.rb +103 -0
  61. data/spec/support/spec_support.rb +176 -0
  62. data/spec/unit/get_private_key_spec.rb +93 -0
  63. metadata +162 -0
@@ -0,0 +1,131 @@
1
+ require 'cheffish/key_formatter'
2
+ require 'cheffish/chef_provider_base'
3
+
4
+ class Cheffish::ActorProviderBase < Cheffish::ChefProviderBase
5
+
6
+ def create_actor
7
+ if new_resource.before
8
+ new_resource.before.call(new_resource)
9
+ end
10
+
11
+ # Create or update the client/user
12
+ current_public_key = new_json['public_key']
13
+ differences = json_differences(current_json, new_json)
14
+ if current_resource_exists?
15
+ # Update the actor if it's different
16
+ if differences.size > 0
17
+ description = [ "update #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences
18
+ converge_by description do
19
+ result = rest.put("#{actor_path}/#{new_resource.name}", normalize_for_put(new_json))
20
+ current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key']
21
+ end
22
+ end
23
+ else
24
+ # Create the actor if it's missing
25
+ if !new_public_key
26
+ raise "You must specify a public key to create a #{actor_type}! Use the private_key resource to create a key, and pass it in with source_key_path."
27
+ end
28
+ description = [ "create #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences
29
+ converge_by description do
30
+ result = rest.post("#{actor_path}", normalize_for_post(new_json))
31
+ current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key']
32
+ end
33
+ end
34
+
35
+ # Write out the public key
36
+ if new_resource.output_key_path
37
+ # TODO use inline_resource
38
+ key_content = Cheffish::KeyFormatter.encode(current_public_key, { :format => new_resource.output_key_format })
39
+ if !current_resource.output_key_path
40
+ action = 'create'
41
+ elsif key_content != IO.read(current_resource.output_key_path)
42
+ action = 'overwrite'
43
+ else
44
+ action = nil
45
+ end
46
+ if action
47
+ converge_by "#{action} public key #{new_resource.output_key_path}" do
48
+ IO.write(new_resource.output_key_path, key_content)
49
+ end
50
+ end
51
+ # TODO permissions?
52
+ end
53
+
54
+ if new_resource.after
55
+ new_resource.after.call(self, new_json, server_private_key, server_public_key)
56
+ end
57
+ end
58
+
59
+ def delete_actor
60
+ if current_resource_exists?
61
+ converge_by "delete #{actor_type} #{new_resource.name} at #{actor_path}" do
62
+ rest.delete("#{actor_path}/#{new_resource.name}")
63
+ Chef::Log.info("#{new_resource} deleted #{actor_type} #{new_resource.name} at #{rest.url}")
64
+ end
65
+ end
66
+ if current_resource.output_key_path
67
+ converge_by "delete public key #{current_resource.output_key_path}" do
68
+ ::File.unlink(current_resource.output_key_path)
69
+ end
70
+ end
71
+ end
72
+
73
+ def new_public_key
74
+ @new_public_key ||= begin
75
+ if new_resource.source_key
76
+ if new_resource.source_key.is_a?(String)
77
+ key, key_format = Cheffish::KeyFormatter.decode(new_resource.source_key)
78
+
79
+ if key.private?
80
+ key.public_key
81
+ else
82
+ key
83
+ end
84
+ elsif new_resource.source_key.private?
85
+ new_resource.source_key.public_key
86
+ else
87
+ new_resource.source_key
88
+ end
89
+ elsif new_resource.source_key_path
90
+ source_key_path = new_resource.source_key_path
91
+ if Pathname.new(source_key_path).relative?
92
+ source_key_str, source_key_path = Cheffish.get_private_key_with_path(source_key_path, run_context.config)
93
+ else
94
+ source_key_str = IO.read(source_key_path)
95
+ end
96
+ source_key, source_key_format = Cheffish::KeyFormatter.decode(source_key_str, new_resource.source_key_pass_phrase, source_key_path)
97
+ if source_key.private?
98
+ source_key.public_key
99
+ else
100
+ source_key
101
+ end
102
+ else
103
+ nil
104
+ end
105
+ end
106
+ end
107
+
108
+ def augment_new_json(json)
109
+ if new_public_key
110
+ json['public_key'] = new_public_key.to_pem
111
+ end
112
+ json
113
+ end
114
+
115
+ def load_current_resource
116
+ begin
117
+ json = rest.get("#{actor_path}/#{new_resource.name}")
118
+ @current_resource = json_to_resource(json)
119
+ rescue Net::HTTPServerException => e
120
+ if e.response.code == "404"
121
+ @current_resource = not_found_resource
122
+ else
123
+ raise
124
+ end
125
+ end
126
+
127
+ if new_resource.output_key_path && ::File.exist?(new_resource.output_key_path)
128
+ current_resource.output_key_path = new_resource.output_key_path
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,115 @@
1
+ require 'cheffish/version'
2
+ require 'chef/dsl/recipe'
3
+ require 'chef/event_dispatch/base'
4
+ require 'chef/event_dispatch/dispatcher'
5
+ require 'chef/node'
6
+ require 'chef/run_context'
7
+ require 'chef/runner'
8
+ require 'forwardable'
9
+ require 'chef/providers'
10
+ require 'chef/resources'
11
+
12
+ module Cheffish
13
+ class BasicChefClient
14
+ include Chef::DSL::Recipe
15
+
16
+ def initialize(node = nil, events = nil)
17
+ if !node
18
+ node = Chef::Node.new
19
+ node.name 'basic_chef_client'
20
+ node.automatic[:platform] = 'basic_chef_client'
21
+ node.automatic[:platform_version] = Cheffish::VERSION
22
+ end
23
+
24
+ @event_catcher = BasicChefClientEvents.new
25
+ dispatcher = Chef::EventDispatch::Dispatcher.new(@event_catcher)
26
+ dispatcher.register(events) if events
27
+ @run_context = Chef::RunContext.new(node, {}, dispatcher)
28
+ @updated = []
29
+ @cookbook_name = 'basic_chef_client'
30
+ end
31
+
32
+ extend Forwardable
33
+
34
+ # Stuff recipes need
35
+ attr_reader :run_context
36
+ attr_accessor :cookbook_name
37
+ attr_accessor :recipe_name
38
+ def_delegators :@run_context, :resource_collection, :immediate_notifications, :delayed_notifications
39
+
40
+ def add_resource(resource)
41
+ resource.run_context = run_context
42
+ run_context.resource_collection.insert(resource)
43
+ end
44
+
45
+ def load_block(&block)
46
+ @recipe_name = 'block'
47
+ instance_eval(&block)
48
+ end
49
+
50
+ def converge
51
+ Chef::Runner.new(self).converge
52
+ end
53
+
54
+ def updates
55
+ @event_catcher.updates
56
+ end
57
+
58
+ def updated?
59
+ @event_catcher.updates.size > 0
60
+ end
61
+
62
+ # Builds a resource sans context, which can be later used in a new client's
63
+ # add_resource() method.
64
+ def self.build_resource(type, name, created_at=nil, &resource_attrs_block)
65
+ created_at ||= caller[0]
66
+ result = BasicChefClient.new.build_resource(type, name, created_at, &resource_attrs_block)
67
+ result
68
+ end
69
+
70
+ def self.inline_resource(provider, provider_action, *resources, &block)
71
+ events = ProviderEventForwarder.new(provider, provider_action)
72
+ client = BasicChefClient.new(provider.node, events)
73
+ resources.each do |resource|
74
+ client.add_resource(resource)
75
+ end
76
+ client.load_block(&block) if block
77
+ client.converge
78
+ client.updated?
79
+ end
80
+
81
+ def self.converge_block(node = nil, events = nil, &block)
82
+ client = BasicChefClient.new(node, events)
83
+ client.load_block(&block)
84
+ client.converge
85
+ client.updated?
86
+ end
87
+
88
+ class BasicChefClientEvents < Chef::EventDispatch::Base
89
+ def initialize
90
+ @updates = []
91
+ end
92
+
93
+ attr_reader :updates
94
+
95
+ # Called after a resource has been completely converged.
96
+ def resource_updated(resource, action)
97
+ updates << [ resource, action ]
98
+ end
99
+ end
100
+
101
+ class ProviderEventForwarder < Chef::EventDispatch::Base
102
+ def initialize(provider, provider_action)
103
+ @provider = provider
104
+ @provider_action = provider_action
105
+ end
106
+
107
+ attr_reader :provider
108
+ attr_reader :provider_action
109
+
110
+ def resource_update_applied(resource, action, update)
111
+ provider.run_context.events.resource_update_applied(provider.new_resource, provider_action, update)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,231 @@
1
+ require 'chef/config'
2
+ require 'chef/run_list'
3
+ require 'chef/provider/lwrp_base'
4
+
5
+ module Cheffish
6
+ class ChefProviderBase < Chef::Provider::LWRPBase
7
+ def rest
8
+ @rest ||= Cheffish.chef_server_api(new_resource.chef_server)
9
+ end
10
+
11
+ def current_resource_exists?
12
+ Array(current_resource.action) != [ :delete ]
13
+ end
14
+
15
+ def not_found_resource
16
+ resource = resource_class.new(new_resource.name, run_context)
17
+ resource.action :delete
18
+ resource
19
+ end
20
+
21
+ def normalize_for_put(json)
22
+ data_handler.normalize_for_put(json, fake_entry)
23
+ end
24
+
25
+ def normalize_for_post(json)
26
+ data_handler.normalize_for_post(json, fake_entry)
27
+ end
28
+
29
+ def new_json
30
+ @new_json ||= begin
31
+ if new_resource.complete
32
+ result = normalize(resource_to_json(new_resource))
33
+ else
34
+ # If resource is incomplete, use current json to fill any holes
35
+ result = Chef::Mixin::DeepMerge.hash_only_merge(current_json, resource_to_json(new_resource))
36
+ end
37
+ augment_new_json(result)
38
+ end
39
+ end
40
+
41
+ # Meant to be overridden
42
+ def augment_new_json(json)
43
+ json
44
+ end
45
+
46
+ def current_json
47
+ @current_json ||= begin
48
+ result = normalize(resource_to_json(current_resource))
49
+ result = augment_current_json(result)
50
+ result
51
+ end
52
+ end
53
+
54
+ # Meant to be overridden
55
+ def augment_current_json(json)
56
+ json
57
+ end
58
+
59
+ def resource_to_json(resource)
60
+ json = resource.raw_json || {}
61
+ keys.each do |json_key, resource_key|
62
+ value = resource.send(resource_key)
63
+ json[json_key] = value if value
64
+ end
65
+ json
66
+ end
67
+
68
+ def json_to_resource(json)
69
+ resource = resource_class.new(new_resource.name, run_context)
70
+ keys.each do |json_key, resource_key|
71
+ resource.send(resource_key, json.delete(json_key))
72
+ end
73
+ # Set the leftover to raw_json
74
+ resource.raw_json json
75
+ resource
76
+ end
77
+
78
+ def normalize(json)
79
+ data_handler.normalize(json, fake_entry)
80
+ end
81
+
82
+ def json_differences(old_json, new_json, print_values=true, name = '', result = nil)
83
+ result ||= []
84
+ json_differences_internal(old_json, new_json, print_values, name, result)
85
+ result
86
+ end
87
+
88
+ def json_differences_internal(old_json, new_json, print_values, name, result)
89
+ if old_json.kind_of?(Hash) && new_json.kind_of?(Hash)
90
+ removed_keys = old_json.keys.inject({}) { |hash, key| hash[key] = true; hash }
91
+ new_json.each_pair do |new_key, new_value|
92
+ if old_json.has_key?(new_key)
93
+ removed_keys.delete(new_key)
94
+ if new_value != old_json[new_key]
95
+ json_differences_internal(old_json[new_key], new_value, print_values, name == '' ? new_key : "#{name}.#{new_key}", result)
96
+ end
97
+ else
98
+ if print_values
99
+ result << " add #{name == '' ? new_key : "#{name}.#{new_key}"} = #{new_value.inspect}"
100
+ else
101
+ result << " add #{name == '' ? new_key : "#{name}.#{new_key}"}"
102
+ end
103
+ end
104
+ end
105
+ removed_keys.keys.each do |removed_key|
106
+ result << " remove #{name == '' ? removed_key : "#{name}.#{removed_key}"}"
107
+ end
108
+ else
109
+ old_json = old_json.to_s if old_json.kind_of?(Symbol)
110
+ new_json = new_json.to_s if new_json.kind_of?(Symbol)
111
+ if old_json != new_json
112
+ if print_values
113
+ result << " update #{name} from #{old_json.inspect} to #{new_json.inspect}"
114
+ else
115
+ result << " update #{name}"
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def apply_modifiers(modifiers, json)
122
+ return json if !modifiers || modifiers.size == 0
123
+
124
+ # If the attributes have nothing, set them to {} so we have something to add to
125
+ if json
126
+ json = Marshal.load(Marshal.dump(json)) # Deep copy
127
+ else
128
+ json = {}
129
+ end
130
+
131
+ modifiers.each do |path, value|
132
+ path = [path] if !path.kind_of?(Array)
133
+ path = path.map { |path_part| path_part.to_s }
134
+ parent = path[0..-2].inject(json) { |hash, path_part| hash ? hash[path_part] : nil }
135
+ existing_value = parent ? parent[path[-1]] : nil
136
+
137
+ if value.is_a?(Proc)
138
+ value = value.call(existing_value)
139
+ end
140
+ if value == :delete
141
+ parent.delete(path[-1]) if parent
142
+ # TODO clean up parent chain if hash is completely emptied
143
+ else
144
+ if !parent
145
+ # Create parent if necessary
146
+ parent = path[0..-2].inject(json) do |hash, path_part|
147
+ hash[path_part] = {} if !hash[path_part]
148
+ hash[path_part]
149
+ end
150
+ end
151
+ parent[path[-1]] = value
152
+ end
153
+ end
154
+ json
155
+ end
156
+
157
+ def apply_run_list_modifiers(add_to_run_list, delete_from_run_list, run_list)
158
+ return run_list if (!add_to_run_list || add_to_run_list.size == 0) && (!delete_from_run_list || !delete_from_run_list.size)
159
+ delete_from_run_list ||= []
160
+ add_to_run_list ||= []
161
+
162
+ run_list = Chef::RunList.new(*run_list)
163
+
164
+ result = []
165
+ add_to_run_list_index = 0
166
+ run_list_index = 0
167
+ while run_list_index < run_list.run_list_items.size do
168
+ # See if the desired run list has this item
169
+ found_desired = add_to_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) }
170
+ if found_desired
171
+ # If so, copy all items up to that desired run list (to preserve order).
172
+ # If a run list item is out of order (run_list = X, B, Y, A, Z and desired = A, B)
173
+ # then this will give us X, A, B. When A is found later, nothing will be copied
174
+ # because found_desired will be less than add_to_run_list_index. The result will
175
+ # be X, A, B, Y, Z.
176
+ if found_desired >= add_to_run_list_index
177
+ result += add_to_run_list[add_to_run_list_index..found_desired].map { |item| item.to_s }
178
+ add_to_run_list_index = found_desired+1
179
+ end
180
+ else
181
+ # If not, just copy it in
182
+ unless delete_from_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) }
183
+ result << run_list[run_list_index].to_s
184
+ end
185
+ end
186
+ run_list_index += 1
187
+ end
188
+
189
+ # Copy any remaining desired items at the end
190
+ result += add_to_run_list[add_to_run_list_index..-1].map { |item| item.to_s }
191
+ result
192
+ end
193
+
194
+ def same_run_list_item(a, b)
195
+ a_name = a.name
196
+ b_name = b.name
197
+ # Handle "a::default" being the same as "a"
198
+ if a.type == :recipe && a_name =~ /(.+)::default$/
199
+ a_name = $1
200
+ elsif b.type == :recipe && b_name =~ /(.+)::default$/
201
+ b_name = $1
202
+ end
203
+
204
+ a_name == b_name && a.type == b.type # We want to replace things with same name and different version
205
+ end
206
+
207
+ private
208
+
209
+ # Needed to be able to use DataHandler classes
210
+ def fake_entry
211
+ FakeEntry.new("#{new_resource.send(keys.values.first)}.json")
212
+ end
213
+
214
+ class FakeEntry
215
+ def initialize(name, parent = nil)
216
+ @name = name
217
+ @parent = parent
218
+ @org = nil
219
+ end
220
+
221
+ attr_reader :name
222
+ attr_reader :parent
223
+ attr_reader :org
224
+ end
225
+ end
226
+
227
+ # We are not interested in Chef's cloning behavior here.
228
+ def load_prior_resource
229
+ Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP")
230
+ end
231
+ end