Class: Chef::PolicyBuilder::Policyfile

Inherits:
Object
  • Object
show all
Defined in:
lib/chef/policy_builder/policyfile.rb

Overview

Policyfile is an experimental policy builder implementation that gets run list and cookbook version information from a single document.

WARNING

This implementation is experimental. It may be changed in incompatible ways in minor or even patch releases, or even abandoned altogether. If using this with other tools, you may be forced to upgrade those tools in lockstep with chef-client because of incompatible behavior changes.

Unsupported Options:

  • override_runlist

    This could potentially be integrated into the

policyfile, or replaced with a similar feature that has different semantics.

  • specific_recipes

    put more design thought into this use case.

  • run_list in json_attribs

    would be ignored anyway, so it raises an error.

  • chef-solo

    not currently supported. Need more design thought around

how this should work.

Defined Under Namespace

Classes: ConfigurationError, PolicyfileError, RunListExpansionIsh, UnsupportedFeature

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node_name, ohai_data, json_attribs, override_runlist, events) ⇒ Policyfile

Returns a new instance of Policyfile.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/chef/policy_builder/policyfile.rb', line 63

def initialize(node_name, ohai_data, json_attribs, override_runlist, events)
  @node_name = node_name
  @ohai_data = ohai_data
  @json_attribs = json_attribs
  @events = events

  @node = nil

  Chef::Log.warn("Using experimental Policyfile feature")

  if Chef::Config[:solo]
    raise UnsupportedFeature, "Policyfile does not support chef-solo at this time."
  end

  if override_runlist
    raise UnsupportedFeature, "Policyfile does not support override run lists at this time"
  end

  if json_attribs && json_attribs.key?("run_list")
    raise UnsupportedFeature, "Policyfile does not support setting the run_list in json data at this time"
  end

  if Chef::Config[:environment] && !Chef::Config[:environment].chomp.empty?
    raise UnsupportedFeature, "Policyfile does not work with Chef Environments"
  end
end

Instance Attribute Details

#eventsObject (readonly)

Returns the value of attribute events.



56
57
58
# File 'lib/chef/policy_builder/policyfile.rb', line 56

def events
  @events
end

#json_attribsObject (readonly)

Returns the value of attribute json_attribs.



60
61
62
# File 'lib/chef/policy_builder/policyfile.rb', line 60

def json_attribs
  @json_attribs
end

#nodeObject (readonly)

Returns the value of attribute node.



57
58
59
# File 'lib/chef/policy_builder/policyfile.rb', line 57

def node
  @node
end

#node_nameObject (readonly)

Returns the value of attribute node_name.



58
59
60
# File 'lib/chef/policy_builder/policyfile.rb', line 58

def node_name
  @node_name
end

#ohai_dataObject (readonly)

Returns the value of attribute ohai_data.



59
60
61
# File 'lib/chef/policy_builder/policyfile.rb', line 59

def ohai_data
  @ohai_data
end

#run_contextObject (readonly)

Returns the value of attribute run_context.



61
62
63
# File 'lib/chef/policy_builder/policyfile.rb', line 61

def run_context
  @run_context
end

Instance Method Details

#apply_policyfile_attributesObject



211
212
213
214
# File 'lib/chef/policy_builder/policyfile.rb', line 211

def apply_policyfile_attributes
  node.attributes.role_default = policy["default_attributes"]
  node.attributes.role_override = policy["override_attributes"]
end

#build_nodeObject

Applies environment, external JSON attributes, and override run list to the node, Then expands the run_list.

Returns

node<Chef::Node>

The modified node object. node is modified in place.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/chef/policy_builder/policyfile.rb', line 133

def build_node
  # consume_external_attrs may add items to the run_list. Save the
  # expanded run_list, which we will pass to the server later to
  # determine which versions of cookbooks to use.
  node.reset_defaults_and_overrides

  node.consume_external_attrs(ohai_data, json_attribs)

  expand_run_list
  apply_policyfile_attributes

  Chef::Log.info("Run List is [#{run_list}]")
  Chef::Log.info("Run List expands to [#{run_list_with_versions_for_display.join(', ')}]")


  events.node_load_completed(node, run_list_with_versions_for_display, Chef::Config)

  node
rescue Exception => e
  events.node_load_failed(node_name, e, Chef::Config)
  raise
end

#configObject



334
335
336
# File 'lib/chef/policy_builder/policyfile.rb', line 334

def config
  Chef::Config
end

#cookbook_lock_for(cookbook_name) ⇒ Object



225
226
227
# File 'lib/chef/policy_builder/policyfile.rb', line 225

def cookbook_lock_for(cookbook_name)
  cookbook_locks[cookbook_name]
end

#cookbook_locksObject



326
327
328
# File 'lib/chef/policy_builder/policyfile.rb', line 326

def cookbook_locks
  policy["cookbook_locks"]
end

#cookbooks_to_syncObject

Builds a ‘cookbook_hash’ map of the form

"COOKBOOK_NAME" => "IDENTIFIER"

This can be passed to a Chef::CookbookSynchronizer object to synchronize the cookbooks.

TODO: Currently this makes N API calls to the server to get the cookbook objects. With server support (bulk API or the like), this should be reduced to a single call.



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/chef/policy_builder/policyfile.rb', line 293

def cookbooks_to_sync
  @cookbook_to_sync ||= begin
    events.cookbook_resolution_start(run_list_with_versions_for_display)

    cookbook_versions_by_name = cookbook_locks.inject({}) do |cb_map, (name, lock_data)|
      cb_map[name] = manifest_for(name, lock_data)
      cb_map
    end
    events.cookbook_resolution_complete(cookbook_versions_by_name)

    cookbook_versions_by_name
  end
rescue Exception => e
  # TODO: wrap/munge exception to provide helpful error output
  events.cookbook_resolution_failed(run_list_with_versions_for_display, e)
  raise
end

#deployment_groupObject



279
280
281
282
# File 'lib/chef/policy_builder/policyfile.rb', line 279

def deployment_group
  Chef::Config[:deployment_group] or
    raise ConfigurationError, "Setting `deployment_group` is not configured."
end

#expand_run_listObject



167
168
169
170
171
172
# File 'lib/chef/policy_builder/policyfile.rb', line 167

def expand_run_list
  node.run_list(run_list)
  node.automatic_attrs[:roles] = []
  node.automatic_attrs[:recipes] = run_list_expansion_ish.recipes
  run_list_expansion_ish
end

#http_apiObject



330
331
332
# File 'lib/chef/policy_builder/policyfile.rb', line 330

def http_api
  @api_service ||= Chef::REST.new(config[:chef_server_url])
end

#load_nodeObject

Loads the node state from the server.



116
117
118
119
120
121
122
123
124
125
126
# File 'lib/chef/policy_builder/policyfile.rb', line 116

def load_node
  events.node_load_start(node_name, Chef::Config)
  Chef::Log.debug("Building node object for #{node_name}")

  @node = Chef::Node.find_or_create(node_name)
  validate_policyfile
  node
rescue Exception => e
  events.node_load_failed(node_name, e, Chef::Config)
  raise
end

#manifest_for(cookbook_name, lock_data) ⇒ Object

Fetches the CookbookVersion object for the given name and identifer specified in the lock_data. TODO: This only implements Chef 11 compatibility mode, which means that cookbooks are fetched by the “dotted_decimal_identifier”: a representation of a SHA1 in the traditional x.y.z version format.



316
317
318
319
320
321
322
323
324
# File 'lib/chef/policy_builder/policyfile.rb', line 316

def manifest_for(cookbook_name, lock_data)
  xyz_version = lock_data["dotted_decimal_identifier"]
  http_api.get("cookbooks/#{cookbook_name}/#{xyz_version}")
rescue Exception => e
  message = "Error loading cookbook #{cookbook_name} at version #{xyz_version}: #{e.class} - #{e.message}"
  err = Chef::Exceptions::CookbookNotFound.new(message)
  err.set_backtrace(e.backtrace)
  raise err
end

#original_runlistObject

Override run_list is not supported.



94
95
96
# File 'lib/chef/policy_builder/policyfile.rb', line 94

def original_runlist
  nil
end

#override_runlistObject

Override run_list is not supported.



99
100
101
# File 'lib/chef/policy_builder/policyfile.rb', line 99

def override_runlist
  nil
end

#parse_recipe_spec(recipe_spec) ⇒ Object



216
217
218
219
220
221
222
223
# File 'lib/chef/policy_builder/policyfile.rb', line 216

def parse_recipe_spec(recipe_spec)
  rmatch = recipe_spec.match(/recipe\[([^:]+)::([^:]+)\]/)
  if rmatch.nil?
    raise PolicyfileError, "invalid recipe specification #{recipe_spec} in Policyfile from #{policyfile_location}"
  else
    [rmatch[1], rmatch[2]]
  end
end

#policyObject



233
234
235
236
237
# File 'lib/chef/policy_builder/policyfile.rb', line 233

def policy
  @policy ||= http_api.get(policyfile_location)
rescue Net::HTTPServerException => e
  raise ConfigurationError, "Error loading policyfile from `#{policyfile_location}': #{e.class} - #{e.message}"
end

#policyfile_locationObject



239
240
241
# File 'lib/chef/policy_builder/policyfile.rb', line 239

def policyfile_location
  "data/policyfiles/#{deployment_group}"
end

#run_listObject



229
230
231
# File 'lib/chef/policy_builder/policyfile.rb', line 229

def run_list
  policy["run_list"]
end

#run_list_expansionObject

Policyfile gives you the run_list already expanded, but users of this class may expect to get a run_list expansion compatible object by calling this method.

Returns

RunListExpansionIsh

A RunListExpansion duck type



109
110
111
# File 'lib/chef/policy_builder/policyfile.rb', line 109

def run_list_expansion
  run_list_expansion_ish
end

#run_list_expansion_ishObject



203
204
205
206
207
208
209
# File 'lib/chef/policy_builder/policyfile.rb', line 203

def run_list_expansion_ish
  recipes = run_list.map do |recipe_spec|
    cookbook, recipe = parse_recipe_spec(recipe_spec)
    "#{cookbook}::#{recipe}"
  end
  RunListExpansionIsh.new(recipes, [])
end

#run_list_with_versions_for_displayObject

Internal Public API ##



194
195
196
197
198
199
200
201
# File 'lib/chef/policy_builder/policyfile.rb', line 194

def run_list_with_versions_for_display
  run_list.map do |recipe_spec|
    cookbook, recipe = parse_recipe_spec(recipe_spec)
    lock_data = cookbook_lock_for(cookbook)
    display = "#{cookbook}::#{recipe}@#{lock_data["version"]} (#{lock_data["identifier"][0...7]})"
    display
  end
end

#setup_run_context(specific_recipes = nil) ⇒ Object



156
157
158
159
160
161
162
163
164
165
# File 'lib/chef/policy_builder/policyfile.rb', line 156

def setup_run_context(specific_recipes=nil)
  Chef::Cookbook::FileVendor.fetch_from_remote(http_api)
  sync_cookbooks
  cookbook_collection = Chef::CookbookCollection.new(cookbooks_to_sync)
  run_context = Chef::RunContext.new(node, cookbook_collection, events)

  run_context.load(run_list_expansion_ish)

  run_context
end

#sync_cookbooksObject



175
176
177
178
179
180
181
182
183
184
# File 'lib/chef/policy_builder/policyfile.rb', line 175

def sync_cookbooks
  Chef::Log.debug("Synchronizing cookbooks")
  synchronizer = Chef::CookbookSynchronizer.new(cookbooks_to_sync, events)
  synchronizer.sync_cookbooks

  # register the file cache path in the cookbook path so that CookbookLoader actually picks up the synced cookbooks
  Chef::Config[:cookbook_path] = File.join(Chef::Config[:file_cache_path], "cookbooks")

  cookbooks_to_sync
end

#temporary_policy?Boolean

Whether or not this is a temporary policy. Since PolicyBuilder doesn’t support override_runlist, this is always false.

Returns:

  • (Boolean)


188
189
190
# File 'lib/chef/policy_builder/policyfile.rb', line 188

def temporary_policy?
  false
end

#validate_policyfileObject

Do some mimimal validation of the policyfile we fetched from the server. Compatibility mode relies on using data bags to store policy files; therefore no real validation will be performed server-side and we need to make additional checks to ensure the data will be formatted correctly.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/chef/policy_builder/policyfile.rb', line 248

def validate_policyfile
  errors = []
  unless run_list
    errors << "Policyfile is missing run_list element"
  end
  unless policy.key?("cookbook_locks")
    errors << "Policyfile is missing cookbook_locks element"
  end
  if run_list.kind_of?(Array)
    run_list_errors = run_list.select do |maybe_recipe_spec|
      validate_recipe_spec(maybe_recipe_spec)
    end
    errors += run_list_errors
  else
    errors << "Policyfile run_list is malformed, must be an array of `recipe[cb_name::recipe_name]` items: #{policy["run_list"]}"
  end

  unless errors.empty?
    raise PolicyfileError, "Policyfile fetched from #{policyfile_location} was invalid:\n#{errors.join("\n")}"
  end
end

#validate_recipe_spec(recipe_spec) ⇒ Object



270
271
272
273
274
275
# File 'lib/chef/policy_builder/policyfile.rb', line 270

def validate_recipe_spec(recipe_spec)
  parse_recipe_spec(recipe_spec)
  nil
rescue PolicyfileError => e
  e.message
end