bolt 2.6.0 → 2.11.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/Puppetfile +4 -3
  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 +1 -1
  9. data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +192 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +122 -0
  11. data/bolt-modules/boltlib/types/planresult.pp +12 -1
  12. data/bolt-modules/file/lib/puppet/functions/file/exists.rb +3 -1
  13. data/bolt-modules/file/lib/puppet/functions/file/join.rb +1 -1
  14. data/bolt-modules/file/lib/puppet/functions/file/read.rb +2 -1
  15. data/bolt-modules/file/lib/puppet/functions/file/readable.rb +3 -1
  16. data/bolt-modules/file/lib/puppet/functions/file/write.rb +3 -1
  17. data/bolt-modules/prompt/lib/puppet/functions/prompt.rb +43 -0
  18. data/lib/bolt/analytics.rb +1 -1
  19. data/lib/bolt/applicator.rb +3 -2
  20. data/lib/bolt/apply_inventory.rb +1 -1
  21. data/lib/bolt/apply_result.rb +1 -1
  22. data/lib/bolt/apply_target.rb +11 -2
  23. data/lib/bolt/bolt_option_parser.rb +27 -7
  24. data/lib/bolt/catalog.rb +32 -3
  25. data/lib/bolt/cli.rb +52 -22
  26. data/lib/bolt/config.rb +51 -27
  27. data/lib/bolt/config/transport/base.rb +3 -3
  28. data/lib/bolt/config/transport/docker.rb +7 -1
  29. data/lib/bolt/config/transport/local.rb +9 -1
  30. data/lib/bolt/config/transport/orch.rb +4 -2
  31. data/lib/bolt/config/transport/remote.rb +2 -0
  32. data/lib/bolt/config/transport/ssh.rb +81 -3
  33. data/lib/bolt/config/transport/winrm.rb +6 -1
  34. data/lib/bolt/executor.rb +38 -0
  35. data/lib/bolt/inventory.rb +2 -1
  36. data/lib/bolt/inventory/group.rb +1 -0
  37. data/lib/bolt/inventory/inventory.rb +9 -0
  38. data/lib/bolt/inventory/target.rb +17 -1
  39. data/lib/bolt/node/output.rb +1 -1
  40. data/lib/bolt/outputter/human.rb +5 -4
  41. data/lib/bolt/outputter/json.rb +1 -1
  42. data/lib/bolt/pal.rb +32 -14
  43. data/lib/bolt/pal/yaml_plan.rb +1 -0
  44. data/lib/bolt/plugin.rb +14 -8
  45. data/lib/bolt/plugin/env_var.rb +2 -1
  46. data/lib/bolt/plugin/module.rb +40 -7
  47. data/lib/bolt/plugin/prompt.rb +1 -1
  48. data/lib/bolt/plugin/puppetdb.rb +5 -2
  49. data/lib/bolt/project.rb +135 -0
  50. data/lib/bolt/puppetdb/config.rb +16 -28
  51. data/lib/bolt/rerun.rb +1 -1
  52. data/lib/bolt/resource_instance.rb +126 -0
  53. data/lib/bolt/result.rb +46 -23
  54. data/lib/bolt/result_set.rb +2 -5
  55. data/lib/bolt/secret.rb +20 -4
  56. data/lib/bolt/shell/bash.rb +27 -14
  57. data/lib/bolt/shell/bash/tmpdir.rb +1 -1
  58. data/lib/bolt/shell/powershell.rb +43 -15
  59. data/lib/bolt/shell/powershell/snippets.rb +1 -1
  60. data/lib/bolt/target.rb +18 -2
  61. data/lib/bolt/transport/base.rb +24 -8
  62. data/lib/bolt/transport/docker.rb +3 -3
  63. data/lib/bolt/transport/docker/connection.rb +11 -7
  64. data/lib/bolt/transport/local/connection.rb +13 -7
  65. data/lib/bolt/transport/orch.rb +5 -1
  66. data/lib/bolt/transport/ssh.rb +6 -2
  67. data/lib/bolt/transport/ssh/connection.rb +26 -1
  68. data/lib/bolt/transport/ssh/exec_connection.rb +110 -0
  69. data/lib/bolt/transport/winrm/connection.rb +10 -2
  70. data/lib/bolt/version.rb +1 -1
  71. data/lib/bolt_server/pe/pal.rb +1 -38
  72. data/lib/bolt_server/transport_app.rb +7 -7
  73. data/lib/bolt_spec/bolt_context.rb +3 -6
  74. data/lib/bolt_spec/plans.rb +78 -8
  75. data/lib/bolt_spec/plans/action_stubs.rb +37 -7
  76. data/lib/bolt_spec/plans/action_stubs/plan_stub.rb +55 -0
  77. data/lib/bolt_spec/plans/mock_executor.rb +62 -2
  78. data/lib/bolt_spec/run.rb +10 -13
  79. metadata +26 -7
  80. data/lib/bolt/boltdir.rb +0 -54
  81. data/lib/bolt/plugin/pkcs7.rb +0 -104
  82. data/lib/bolt/secret/base.rb +0 -41
@@ -19,7 +19,7 @@ module Bolt
19
19
 
20
20
  def resolve_reference(opts)
21
21
  STDERR.print("#{opts['message']}: ")
22
- value = STDIN.noecho(&:gets).chomp
22
+ value = STDIN.noecho(&:gets).to_s.chomp
23
23
  STDERR.puts
24
24
 
25
25
  value
@@ -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
 
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'bolt/pal'
5
+
6
+ module Bolt
7
+ class Project
8
+ BOLTDIR_NAME = 'Boltdir'
9
+ PROJECT_SETTINGS = {
10
+ "name" => "The name of the project",
11
+ "plans" => "An array of plan names to whitelist. Whitelisted plans are included in `bolt plan show` output",
12
+ "tasks" => "An array of task names to whitelist. Whitelisted plans are included in `bolt task show` output"
13
+ }.freeze
14
+
15
+ attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config,
16
+ :puppetfile, :rerunfile, :type, :resource_types
17
+
18
+ def self.default_project
19
+ Project.new(File.join('~', '.puppetlabs', 'bolt'), 'user')
20
+ end
21
+
22
+ # Search recursively up the directory hierarchy for the Project. Look for a
23
+ # directory called Boltdir or a file called bolt.yaml (for a control repo
24
+ # type Project). Otherwise, repeat the check on each directory up the
25
+ # hierarchy, falling back to the default if we reach the root.
26
+ def self.find_boltdir(dir)
27
+ dir = Pathname.new(dir)
28
+ if (dir + BOLTDIR_NAME).directory?
29
+ new(dir + BOLTDIR_NAME, 'embedded')
30
+ elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
31
+ new(dir, 'local')
32
+ elsif dir.root?
33
+ default_project
34
+ else
35
+ find_boltdir(dir.parent)
36
+ end
37
+ end
38
+
39
+ def initialize(path, type = 'option')
40
+ @path = Pathname.new(path).expand_path
41
+ @config_file = @path + 'bolt.yaml'
42
+ @inventory_file = @path + 'inventory.yaml'
43
+ @modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
44
+ @hiera_config = @path + 'hiera.yaml'
45
+ @puppetfile = @path + 'Puppetfile'
46
+ @rerunfile = @path + '.rerun.json'
47
+ @resource_types = @path + '.resource_types'
48
+ @type = type
49
+
50
+ @project_file = @path + 'bolt-project.yaml'
51
+ @data = Bolt::Util.read_optional_yaml_hash(File.expand_path(@project_file), 'project') || {}
52
+ validate if load_as_module?
53
+ end
54
+
55
+ def to_s
56
+ @path.to_s
57
+ end
58
+
59
+ # This API is used to prepend the project as a module to Puppet's internal
60
+ # module_references list. CHANGE AT YOUR OWN RISK
61
+ def to_h
62
+ { path: @path, name: name }
63
+ end
64
+
65
+ def eql?(other)
66
+ path == other.path
67
+ end
68
+ alias == eql?
69
+
70
+ def load_as_module?
71
+ @project_file.file?
72
+ end
73
+
74
+ def name
75
+ # If the project is in mymod/Boltdir/bolt-project.yaml, use mymod as the project name
76
+ dirname = @path.basename.to_s == 'Boltdir' ? @path.parent.basename.to_s : @path.basename.to_s
77
+ pname = @data['name'] || dirname
78
+ pname.include?('-') ? pname.split('-', 2)[1] : pname
79
+ end
80
+
81
+ def tasks
82
+ @data['tasks']
83
+ end
84
+
85
+ def plans
86
+ @data['plans']
87
+ end
88
+
89
+ def project_directory_name?(name)
90
+ # it must match an installed project name according to forge validator
91
+ name =~ /^[a-z][a-z0-9_]*$/
92
+ end
93
+
94
+ def project_namespaced_name?(name)
95
+ # it must match the full project name according to forge validator
96
+ name =~ /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
97
+ end
98
+
99
+ def validate
100
+ n = @data['name']
101
+ if n && !project_directory_name?(n) && !project_namespaced_name?(n)
102
+ raise Bolt::ValidationError, <<~ERROR_STRING
103
+ Invalid project name '#{n}' in bolt-project.yaml; project names must match either:
104
+ An installed project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
105
+ A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
106
+ ERROR_STRING
107
+ elsif !project_directory_name?(name) && !project_namespaced_name?(name)
108
+ raise Bolt::ValidationError, <<~ERROR_STRING
109
+ Invalid project name '#{name}'; project names must match either:
110
+ A project name (ex. projectname) matching the expression /^[a-z][a-z0-9_]*$/ -or-
111
+ A namespaced project name (ex. author-projectname) matching the expression /^[a-zA-Z0-9]+[-][a-z][a-z0-9_]*$/
112
+
113
+ Configure project name in <project_dir>/bolt-project.yaml
114
+ ERROR_STRING
115
+ # If the project name is the same as one of the built-in modules raise a warning
116
+ elsif Dir.children(Bolt::PAL::BOLTLIB_PATH).include?(name)
117
+ raise Bolt::ValidationError, "The project '#{name}' will not be loaded. The project name conflicts "\
118
+ "with a built-in Bolt module of the same name."
119
+ end
120
+
121
+ %w[tasks plans].each do |conf|
122
+ unless @data.fetch(conf, []).is_a?(Array)
123
+ raise Bolt::ValidationError, "'#{conf}' in bolt-project.yaml must be an array"
124
+ end
125
+ end
126
+ end
127
+
128
+ def check_deprecated_file
129
+ if (@path + 'project.yaml').file?
130
+ logger = Logging.logger[self]
131
+ logger.warn "Project configuration file 'project.yaml' is deprecated; use 'bolt-project.yaml' instead."
132
+ end
133
+ end
134
+ end
135
+ end
@@ -22,37 +22,29 @@ 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, boltdir_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', {})
49
- new(config.merge(options), boltdir_path)
42
+ new(config.merge(options), project_path)
50
43
  end
51
44
 
52
- def initialize(settings, boltdir_path = nil)
45
+ def initialize(settings, project_path = nil)
53
46
  @settings = settings
54
- @boltdir_path = boltdir_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 @boltdir_path
75
- File.expand_path(@settings[file], @boltdir_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
@@ -40,18 +40,23 @@ module Bolt
40
40
  end
41
41
 
42
42
  def self.for_task(target, stdout, stderr, exit_code, task)
43
- begin
44
- value = JSON.parse(stdout)
45
- unless value.is_a? Hash
46
- value = nil
47
- end
48
- rescue JSON::ParserError
49
- value = nil
50
- end
51
- value ||= { '_output' => stdout }
43
+ stdout.force_encoding('utf-8') unless stdout.encoding == Encoding::UTF_8
44
+ value = if stdout.valid_encoding?
45
+ parse_hash(stdout) || { '_output' => stdout }
46
+ else
47
+ { '_error' => { 'kind' => 'puppetlabs.tasks/task-error',
48
+ 'issue_code' => 'TASK_ERROR',
49
+ 'msg' => 'The task result contained invalid UTF-8 on stdout',
50
+ 'details' => {} } }
51
+ end
52
+
52
53
  if exit_code != 0 && value['_error'].nil?
53
54
  msg = if stdout.empty?
54
- "The task failed with exit code #{exit_code}:\n#{stderr}"
55
+ if stderr.empty?
56
+ "The task failed with exit code #{exit_code} and no output"
57
+ else
58
+ "The task failed with exit code #{exit_code} and no stdout, but stderr contained:\n#{stderr}"
59
+ end
55
60
  else
56
61
  "The task failed with exit code #{exit_code}"
57
62
  end
@@ -63,6 +68,13 @@ module Bolt
63
68
  new(target, value: value, action: 'task', object: task)
64
69
  end
65
70
 
71
+ def self.parse_hash(string)
72
+ value = JSON.parse(string)
73
+ value if value.is_a? Hash
74
+ rescue JSON::ParserError
75
+ nil
76
+ end
77
+
66
78
  def self.for_upload(target, source, destination)
67
79
  new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", action: 'upload', object: source)
68
80
  end
@@ -110,18 +122,8 @@ module Bolt
110
122
  message && !message.strip.empty?
111
123
  end
112
124
 
113
- def status_hash
114
- {
115
- target: @target.name,
116
- action: action,
117
- object: object,
118
- status: status,
119
- value: @value
120
- }
121
- end
122
-
123
125
  def generic_value
124
- value.reject { |k, _| %w[_error _output].include? k }
126
+ safe_value.reject { |k, _| %w[_error _output].include? k }
125
127
  end
126
128
 
127
129
  def eql?(other)
@@ -139,15 +141,36 @@ module Bolt
139
141
  end
140
142
 
141
143
  def to_json(opts = nil)
142
- status_hash.to_json(opts)
144
+ to_data.to_json(opts)
143
145
  end
144
146
 
145
147
  def to_s
146
148
  to_json
147
149
  end
148
150
 
151
+ # This is the value with all non-UTF-8 characters removed, suitable for
152
+ # printing or converting to JSON. It *should* only be possible to have
153
+ # non-UTF-8 characters in stdout/stderr keys as they are not allowed from
154
+ # tasks but we scrub the whole thing just in case.
155
+ def safe_value
156
+ Bolt::Util.walk_vals(value) do |val|
157
+ if val.is_a?(String)
158
+ # Replace invalid bytes with hex codes, ie. \xDE\xAD\xBE\xEF
159
+ val.scrub { |c| c.bytes.map { |b| "\\x" + b.to_s(16).upcase }.join }
160
+ else
161
+ val
162
+ end
163
+ end
164
+ end
165
+
149
166
  def to_data
150
- Bolt::Util.walk_keys(status_hash, &:to_s)
167
+ {
168
+ "target" => @target.name,
169
+ "action" => action,
170
+ "object" => object,
171
+ "status" => status,
172
+ "value" => safe_value
173
+ }
151
174
  end
152
175
 
153
176
  def status