bolt 2.8.0 → 2.12.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +2 -2
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +27 -0
  5. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
  6. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
  7. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +4 -3
  8. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +61 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +122 -0
  10. data/bolt-modules/boltlib/types/planresult.pp +12 -1
  11. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  12. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  13. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  14. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  15. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  16. data/lib/bolt/analytics.rb +21 -2
  17. data/lib/bolt/applicator.rb +3 -1
  18. data/lib/bolt/apply_result.rb +1 -1
  19. data/lib/bolt/apply_target.rb +3 -2
  20. data/lib/bolt/bolt_option_parser.rb +18 -8
  21. data/lib/bolt/cli.rb +35 -5
  22. data/lib/bolt/config.rb +45 -13
  23. data/lib/bolt/config/transport/docker.rb +2 -0
  24. data/lib/bolt/config/transport/local.rb +2 -0
  25. data/lib/bolt/config/transport/orch.rb +2 -0
  26. data/lib/bolt/config/transport/remote.rb +2 -0
  27. data/lib/bolt/config/transport/ssh.rb +50 -1
  28. data/lib/bolt/config/transport/winrm.rb +2 -0
  29. data/lib/bolt/inventory.rb +2 -1
  30. data/lib/bolt/inventory/group.rb +1 -0
  31. data/lib/bolt/inventory/inventory.rb +5 -0
  32. data/lib/bolt/inventory/target.rb +17 -1
  33. data/lib/bolt/node/output.rb +1 -1
  34. data/lib/bolt/outputter/human.rb +5 -4
  35. data/lib/bolt/outputter/json.rb +1 -1
  36. data/lib/bolt/pal.rb +4 -1
  37. data/lib/bolt/pal/yaml_plan.rb +1 -0
  38. data/lib/bolt/plugin.rb +13 -7
  39. data/lib/bolt/plugin/puppetdb.rb +5 -2
  40. data/lib/bolt/project.rb +25 -7
  41. data/lib/bolt/puppetdb/config.rb +14 -26
  42. data/lib/bolt/rerun.rb +1 -1
  43. data/lib/bolt/resource_instance.rb +126 -0
  44. data/lib/bolt/result.rb +46 -23
  45. data/lib/bolt/result_set.rb +2 -5
  46. data/lib/bolt/shell/bash.rb +1 -1
  47. data/lib/bolt/shell/powershell.rb +12 -4
  48. data/lib/bolt/target.rb +12 -1
  49. data/lib/bolt/transport/ssh.rb +6 -2
  50. data/lib/bolt/transport/ssh/connection.rb +4 -0
  51. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  52. data/lib/bolt/transport/winrm/connection.rb +6 -2
  53. data/lib/bolt/version.rb +1 -1
  54. data/lib/bolt_server/pe/pal.rb +1 -38
  55. data/lib/bolt_server/transport_app.rb +7 -7
  56. data/lib/bolt_spec/bolt_context.rb +1 -4
  57. data/lib/bolt_spec/plans/mock_executor.rb +1 -0
  58. data/lib/bolt_spec/run.rb +2 -5
  59. metadata +6 -2
@@ -7,6 +7,8 @@ module Bolt
7
7
  class Config
8
8
  module Transport
9
9
  class WinRM < Base
10
+ # NOTE: All transport configuration options should have a corresponding schema definition
11
+ # in schemas/bolt-transport-definitions.json
10
12
  OPTIONS = {
11
13
  "basic-auth-only" => { type: TrueClass,
12
14
  desc: "Force basic authentication. This option is only available when using SSL." },
@@ -15,6 +15,7 @@ module Bolt
15
15
 
16
16
  class ValidationError < Bolt::Error
17
17
  attr_accessor :path
18
+
18
19
  def initialize(message, offending_group)
19
20
  super(message, 'bolt.inventory/validation-error')
20
21
  @_message = message
@@ -83,7 +84,7 @@ module Bolt
83
84
 
84
85
  def self.empty
85
86
  config = Bolt::Config.default
86
- plugins = Bolt::Plugin.setup(config, nil, nil, Bolt::Analytics::NoopClient)
87
+ plugins = Bolt::Plugin.setup(config, nil)
87
88
 
88
89
  create_version({}, config.transport, config.transports, plugins)
89
90
  end
@@ -12,6 +12,7 @@ module Bolt
12
12
  # Regex used to validate group names and target aliases.
13
13
  NAME_REGEX = /\A[a-z0-9_][a-z0-9_-]*\Z/.freeze
14
14
 
15
+ # NOTE: All keys should have a corresponding schema property in schemas/bolt-inventory.schema.json
15
16
  DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
16
17
  TARGET_KEYS = DATA_KEYS + %w[name alias uri]
17
18
  GROUP_KEYS = DATA_KEYS + %w[name groups targets]
@@ -7,6 +7,7 @@ module Bolt
7
7
  class Inventory
8
8
  class Inventory
9
9
  attr_reader :targets, :plugins, :config, :transport
10
+
10
11
  class WildcardError < Bolt::Error
11
12
  def initialize(target)
12
13
  super("Found 0 targets matching wildcard pattern #{target}", 'bolt.inventory/wildcard-error')
@@ -318,6 +319,10 @@ module Bolt
318
319
  def target_config(target)
319
320
  @targets[target.name].config
320
321
  end
322
+
323
+ def resources(target)
324
+ @targets[target.name].resources
325
+ end
321
326
  end
322
327
  end
323
328
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bolt/resource_instance'
4
+
3
5
  module Bolt
4
6
  class Inventory
5
7
  # This class represents the active state of a target within the inventory.
6
8
  class Target
7
- attr_reader :name, :uri, :safe_name, :target_alias
9
+ attr_reader :name, :uri, :safe_name, :target_alias, :resources
8
10
 
9
11
  def initialize(target_data, inventory)
10
12
  unless target_data['name'] || target_data['uri']
@@ -36,12 +38,26 @@ module Bolt
36
38
  # When alias is specified in a plan, the key will be `target_alias`, when
37
39
  # alias is specified in inventory the key will be `alias`.
38
40
  @target_alias = target_data['target_alias'] || target_data['alias'] || []
41
+ @resources = {}
39
42
 
40
43
  @inventory = inventory
41
44
 
42
45
  validate
43
46
  end
44
47
 
48
+ # rubocop:disable Naming/AccessorMethodName
49
+ def set_resource(resource)
50
+ if (existing_resource = resources[resource.reference])
51
+ existing_resource.overwrite_state(resource.state)
52
+ existing_resource.overwrite_desired_state(resource.desired_state)
53
+ existing_resource.events = existing_resource.events + resource.events
54
+ existing_resource
55
+ else
56
+ @resources[resource.reference] = resource
57
+ end
58
+ end
59
+ # rubocop:enable Naming/AccessorMethodName
60
+
45
61
  def vars
46
62
  group_cache['vars'].merge(@vars)
47
63
  end
@@ -12,7 +12,7 @@ module Bolt
12
12
  def initialize
13
13
  @stdout = StringIO.new
14
14
  @stderr = StringIO.new
15
- @exit_code = 'unkown'
15
+ @exit_code = 'unknown'
16
16
  end
17
17
  end
18
18
  end
@@ -107,13 +107,14 @@ module Bolt
107
107
 
108
108
  # Use special handling if the result looks like a command or script result
109
109
  if result.generic_value.keys == %w[stdout stderr exit_code]
110
- unless result['stdout'].strip.empty?
110
+ safe_value = result.safe_value
111
+ unless safe_value['stdout'].strip.empty?
111
112
  @stream.puts(indent(2, "STDOUT:"))
112
- @stream.puts(indent(4, result['stdout']))
113
+ @stream.puts(indent(4, safe_value['stdout']))
113
114
  end
114
- unless result['stderr'].strip.empty?
115
+ unless safe_value['stderr'].strip.empty?
115
116
  @stream.puts(indent(2, "STDERR:"))
116
- @stream.puts(indent(4, result['stderr']))
117
+ @stream.puts(indent(4, safe_value['stderr']))
117
118
  end
118
119
  elsif result.generic_value.any?
119
120
  @stream.puts(indent(2, ::JSON.pretty_generate(result.generic_value)))
@@ -28,7 +28,7 @@ module Bolt
28
28
 
29
29
  def print_result(result)
30
30
  @stream.puts ',' if @preceding_item
31
- @stream.puts result.status_hash.to_json
31
+ @stream.puts result.to_json
32
32
  @preceding_item = true
33
33
  end
34
34
 
@@ -329,7 +329,8 @@ module Bolt
329
329
  raise Bolt::Error.unknown_plan(plan_name)
330
330
  end
331
331
 
332
- mod = plan_sig.instance_variable_get(:@plan_func).loader.parent.path
332
+ # path may be a Pathname object, so make sure to stringify it
333
+ mod = plan_sig.instance_variable_get(:@plan_func).loader.parent.path.to_s
333
334
 
334
335
  # If it's a Puppet language plan, use strings to extract data. The only
335
336
  # way to tell is to check which filename exists in the module.
@@ -358,6 +359,7 @@ module Bolt
358
359
  name = param.name
359
360
  if signature_params.include?(name)
360
361
  params[name] = { 'type' => param.types.first }
362
+ params[name]['sensitive'] = param.types.first =~ /\ASensitive(\[.*\])?\z/ ? true : false
361
363
  params[name]['default_value'] = defaults[name] if defaults.key?(name)
362
364
  params[name]['description'] = param.text unless param.text.empty?
363
365
  else
@@ -389,6 +391,7 @@ module Bolt
389
391
  param.type_expr
390
392
  end
391
393
  params[name] = { 'type' => type_str }
394
+ params[name]['sensitive'] = param.type_expr.instance_of?(Puppet::Pops::Types::PSensitiveType)
392
395
  params[name]['default_value'] = param.value
393
396
  params[name]['description'] = param.description if param.description
394
397
  end
@@ -99,6 +99,7 @@ module Bolt
99
99
  # logic.
100
100
  class EvaluableString
101
101
  attr_reader :value
102
+
102
103
  def initialize(value)
103
104
  @value = value
104
105
  end
@@ -119,13 +119,9 @@ module Bolt
119
119
  end
120
120
  end
121
121
 
122
- def self.setup(config, pal, pdb_client, analytics)
122
+ def self.setup(config, pal, analytics = Bolt::Analytics::NoopClient.new)
123
123
  plugins = new(config, pal, analytics)
124
124
 
125
- # PDB is special because it needs the PDB client. Since it has no config,
126
- # we can just add it first.
127
- plugins.add_plugin(Bolt::Plugin::Puppetdb.new(pdb_client))
128
-
129
125
  # Initialize any plugins referenced in plugin config. This will also indirectly
130
126
  # initialize any plugins they depend on.
131
127
  if plugins.reference?(config.plugins)
@@ -142,7 +138,7 @@ module Bolt
142
138
  plugins
143
139
  end
144
140
 
145
- RUBY_PLUGINS = %w[task prompt env_var].freeze
141
+ RUBY_PLUGINS = %w[task prompt env_var puppetdb].freeze
146
142
  BUILTIN_PLUGINS = %w[task terraform pkcs7 prompt vault aws_inventory puppetdb azure_inventory
147
143
  yaml env_var gcloud_inventory].freeze
148
144
  DEFAULT_PLUGIN_HOOKS = { 'puppet_library' => { 'plugin' => 'puppet_agent', 'stop_service' => true } }.freeze
@@ -161,6 +157,13 @@ module Bolt
161
157
  @unknown = Set.new
162
158
  @resolution_stack = []
163
159
  @unresolved_plugin_configs = config.plugins.dup
160
+ # The puppetdb plugin config comes from the puppetdb section, not from
161
+ # the plugins section
162
+ if @unresolved_plugin_configs.key?('puppetdb')
163
+ msg = "Configuration for the PuppetDB plugin must be in the 'puppetdb' config section, not 'plugins'"
164
+ raise Bolt::Error.new(msg, 'bolt/plugin-error')
165
+ end
166
+ @unresolved_plugin_configs['puppetdb'] = config.puppetdb if config.puppetdb
164
167
  @plugin_hooks = DEFAULT_PLUGIN_HOOKS.dup
165
168
  end
166
169
 
@@ -168,7 +171,6 @@ module Bolt
168
171
  @modules ||= Bolt::Module.discover(@pal.modulepath)
169
172
  end
170
173
 
171
- # Generally this is private. Puppetdb is special though
172
174
  def add_plugin(plugin)
173
175
  @plugins[plugin.name] = plugin
174
176
  end
@@ -235,6 +237,10 @@ module Bolt
235
237
  end
236
238
  end
237
239
 
240
+ def puppetdb_client
241
+ by_name('puppetdb').puppetdb_client
242
+ end
243
+
238
244
  # Evaluate all _plugin references in a data structure. Leaves are
239
245
  # evaluated and then their parents are evaluated with references replaced
240
246
  # by their values. If the result of a reference contains more references,
@@ -14,8 +14,11 @@ module Bolt
14
14
  TEMPLATE_OPTS = %w[alias config facts features name uri vars].freeze
15
15
  PLUGIN_OPTS = %w[_plugin query target_mapping].freeze
16
16
 
17
- def initialize(pdb_client)
18
- @puppetdb_client = pdb_client
17
+ attr_reader :puppetdb_client
18
+
19
+ def initialize(config:, context:)
20
+ pdb_config = Bolt::PuppetDB::Config.load_config(config, context.boltdir)
21
+ @puppetdb_client = Bolt::PuppetDB::Client.new(pdb_config)
19
22
  @logger = Logging.logger[self]
20
23
  end
21
24
 
@@ -16,7 +16,18 @@ module Bolt
16
16
  :puppetfile, :rerunfile, :type, :resource_types
17
17
 
18
18
  def self.default_project
19
- Project.new(File.join('~', '.puppetlabs', 'bolt'), 'user')
19
+ Project.new(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
20
+ # If homedir isn't defined use the system config path
21
+ rescue ArgumentError
22
+ Project.new(system_path, 'system')
23
+ end
24
+
25
+ def self.system_path
26
+ if Bolt::Util.windows?
27
+ File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc')
28
+ else
29
+ File.join('/etc', 'puppetlabs', 'bolt')
30
+ end
20
31
  end
21
32
 
22
33
  # Search recursively up the directory hierarchy for the Project. Look for a
@@ -27,7 +38,7 @@ module Bolt
27
38
  dir = Pathname.new(dir)
28
39
  if (dir + BOLTDIR_NAME).directory?
29
40
  new(dir + BOLTDIR_NAME, 'embedded')
30
- elsif (dir + 'bolt.yaml').file?
41
+ elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
31
42
  new(dir, 'local')
32
43
  elsif dir.root?
33
44
  default_project
@@ -47,7 +58,7 @@ module Bolt
47
58
  @resource_types = @path + '.resource_types'
48
59
  @type = type
49
60
 
50
- @project_file = @path + 'project.yaml'
61
+ @project_file = @path + 'bolt-project.yaml'
51
62
  @data = Bolt::Util.read_optional_yaml_hash(File.expand_path(@project_file), 'project') || {}
52
63
  validate if load_as_module?
53
64
  end
@@ -72,7 +83,7 @@ module Bolt
72
83
  end
73
84
 
74
85
  def name
75
- # If the project is in mymod/Boltdir/project.yaml, use mymod as the project name
86
+ # If the project is in mymod/Boltdir/bolt-project.yaml, use mymod as the project name
76
87
  dirname = @path.basename.to_s == 'Boltdir' ? @path.parent.basename.to_s : @path.basename.to_s
77
88
  pname = @data['name'] || dirname
78
89
  pname.include?('-') ? pname.split('-', 2)[1] : pname
@@ -100,7 +111,7 @@ module Bolt
100
111
  n = @data['name']
101
112
  if n && !project_directory_name?(n) && !project_namespaced_name?(n)
102
113
  raise Bolt::ValidationError, <<~ERROR_STRING
103
- Invalid project name '#{n}' in project.yaml; project names must match either:
114
+ Invalid project name '#{n}' in bolt-project.yaml; project names must match either:
104
115
  An installed project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
105
116
  A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
106
117
  ERROR_STRING
@@ -110,7 +121,7 @@ module Bolt
110
121
  A project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
111
122
  A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
112
123
 
113
- Configure project name in <project_dir>/project.yaml
124
+ Configure project name in <project_dir>/bolt-project.yaml
114
125
  ERROR_STRING
115
126
  # If the project name is the same as one of the built-in modules raise a warning
116
127
  elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
@@ -120,9 +131,16 @@ module Bolt
120
131
 
121
132
  %w[tasks plans].each do |conf|
122
133
  unless @data.fetch(conf, []).is_a?(Array)
123
- raise Bolt::ValidationError, "'#{conf}' in project.yaml must be an array"
134
+ raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
124
135
  end
125
136
  end
126
137
  end
138
+
139
+ def check_deprecated_file
140
+ if (@path + 'project.yaml').file?
141
+ logger = Logging.logger[self]
142
+ logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
143
+ end
144
+ end
127
145
  end
128
146
  end
@@ -22,27 +22,20 @@ module Bolt
22
22
  File.expand_path(File.join(Dir::COMMON_APPDATA, 'PuppetLabs/client-tools/puppetdb.conf'))
23
23
  end
24
24
 
25
- def self.load_config(filename, options, project_path = nil)
25
+ def self.load_config(options, project_path = nil)
26
26
  config = {}
27
27
  global_path = Bolt::Util.windows? ? default_windows_config : DEFAULT_CONFIG[:global]
28
- if filename
29
- if File.exist?(filename)
30
- config = JSON.parse(File.read(filename))
31
- else
32
- raise Bolt::PuppetDBError, "config file #{filename} does not exist"
33
- end
34
- else
35
- if File.exist?(DEFAULT_CONFIG[:user])
36
- filepath = DEFAULT_CONFIG[:user]
37
- elsif File.exist?(global_path)
38
- filepath = global_path
39
- end
40
28
 
41
- begin
42
- config = JSON.parse(File.read(filepath)) if filepath
43
- rescue StandardError => e
44
- Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
45
- end
29
+ if File.exist?(DEFAULT_CONFIG[:user])
30
+ filepath = DEFAULT_CONFIG[:user]
31
+ elsif File.exist?(global_path)
32
+ filepath = global_path
33
+ end
34
+
35
+ begin
36
+ config = JSON.parse(File.read(filepath)) if filepath
37
+ rescue StandardError => e
38
+ Logging.logger[self].error("Could not load puppetdb.conf from #{filepath}: #{e.message}")
46
39
  end
47
40
 
48
41
  config = config.fetch('puppetdb', {})
@@ -51,8 +44,7 @@ module Bolt
51
44
 
52
45
  def initialize(settings, project_path = nil)
53
46
  @settings = settings
54
- @project_path = project_path
55
- expand_paths
47
+ expand_paths(project_path)
56
48
  end
57
49
 
58
50
  def token
@@ -68,14 +60,10 @@ module Bolt
68
60
  @token = @token.strip if @token
69
61
  end
70
62
 
71
- def expand_paths
63
+ def expand_paths(project_path)
72
64
  %w[cacert cert key token].each do |file|
73
65
  next unless @settings[file]
74
- @settings[file] = if @project_path
75
- File.expand_path(@settings[file], @project_path)
76
- else
77
- File.expand_path(@settings[file])
78
- end
66
+ @settings[file] = File.expand_path(@settings[file], project_path)
79
67
  end
80
68
  end
81
69
 
@@ -45,7 +45,7 @@ module Bolt
45
45
  end
46
46
 
47
47
  if result_set.is_a?(Bolt::ResultSet)
48
- data = result_set.map { |res| res.status_hash.select { |k, _| %i[target status].include? k } }
48
+ data = result_set.map { |res| { target: res.target.name, status: res.status } }
49
49
  FileUtils.mkdir_p(File.dirname(@path))
50
50
  File.write(@path, data.to_json)
51
51
  elsif File.exist?(@path)
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Bolt
6
+ class ResourceInstance
7
+ attr_reader :target, :type, :title, :state, :desired_state
8
+ attr_accessor :events
9
+
10
+ # Needed by Puppet to recognize Bolt::ResourceInstance as a Puppet object when deserializing
11
+ def self._pcore_type
12
+ ResourceInstance
13
+ end
14
+
15
+ # Needed by Puppet to serialize with _pcore_init_hash instead of the object's attributes
16
+ def self._pcore_init_from_hash(_init_hash)
17
+ raise "ResourceInstance shouldn't be instantiated from a pcore_init class method. "\
18
+ "How did this get called?"
19
+ end
20
+
21
+ def _pcore_init_from_hash(init_hash)
22
+ initialize(init_hash)
23
+ end
24
+
25
+ # Parameters will already be validated when calling ResourceInstance.new or
26
+ # set_resources() from a plan. We don't perform any validation in the class
27
+ # itself since Puppet will pass an empty hash to the initializer as part of
28
+ # the deserialization process before passing the _pcore_init_hash.
29
+ def initialize(resource_hash)
30
+ @target = resource_hash['target']
31
+ @type = resource_hash['type'].to_s.capitalize
32
+ @title = resource_hash['title']
33
+ # get_resources() returns observed state under the 'parameters' key
34
+ @state = resource_hash['state'] || resource_hash['parameters'] || {}
35
+ @desired_state = resource_hash['desired_state'] || {}
36
+ @events = resource_hash['events'] || []
37
+ end
38
+
39
+ # Creates a ResourceInstance from a data hash in a plan when calling
40
+ # ResourceInstance.new($resource_hash) or $target.set_resources($resource_hash)
41
+ def self.from_asserted_hash(resource_hash)
42
+ new(resource_hash)
43
+ end
44
+
45
+ # Creates a ResourceInstance from positional arguments in a plan when
46
+ # calling ResourceInstance.new(target, type, title, ...)
47
+ def self.from_asserted_args(target,
48
+ type,
49
+ title,
50
+ state = nil,
51
+ desired_state = nil,
52
+ events = nil)
53
+ new(
54
+ 'target' => target,
55
+ 'type' => type,
56
+ 'title' => title,
57
+ 'state' => state,
58
+ 'desired_state' => desired_state,
59
+ 'events' => events
60
+ )
61
+ end
62
+
63
+ def eql?(other)
64
+ self.class.equal?(other.class) &&
65
+ target == other.target &&
66
+ type == other.type &&
67
+ title == other.title
68
+ end
69
+ alias == eql?
70
+
71
+ def to_hash
72
+ {
73
+ 'target' => target,
74
+ 'type' => type,
75
+ 'title' => title,
76
+ 'state' => state,
77
+ 'desired_state' => desired_state,
78
+ 'events' => events
79
+ }
80
+ end
81
+ alias _pcore_init_hash to_hash
82
+
83
+ def to_json(opts = nil)
84
+ to_hash.to_json(opts)
85
+ end
86
+
87
+ def reference
88
+ "#{type}[#{title}]"
89
+ end
90
+ alias to_s reference
91
+
92
+ def add_event(event)
93
+ @events << event
94
+ end
95
+
96
+ # rubocop:disable Naming/AccessorMethodName
97
+ def set_state(state)
98
+ assert_hash('state', state)
99
+ @state.merge!(state)
100
+ end
101
+ # rubocop:enable Naming/AccessorMethodName
102
+
103
+ def overwrite_state(state)
104
+ assert_hash('state', state)
105
+ @state = state
106
+ end
107
+
108
+ # rubocop:disable Naming/AccessorMethodName
109
+ def set_desired_state(desired_state)
110
+ assert_hash('desired_state', desired_state)
111
+ @desired_state.merge!(desired_state)
112
+ end
113
+ # rubocop:enable Naming/AccessorMethodName
114
+
115
+ def overwrite_desired_state(desired_state)
116
+ assert_hash('desired_state', desired_state)
117
+ @desired_state = desired_state
118
+ end
119
+
120
+ def assert_hash(loc, value)
121
+ unless value.is_a?(Hash)
122
+ raise Bolt::ValidationError, "#{loc} must be of type Hash; got #{value.class}"
123
+ end
124
+ end
125
+ end
126
+ end