ruby-pwsh 1.1.1 → 1.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ac282abeea132104fda1b3fc2f4743f4b99603cdd6b73a42b7063641c9e65d9
4
- data.tar.gz: 410bc1d7a7b08366a89019324b172219102197f8aa6891bcfaa1729e3bd76925
3
+ metadata.gz: 106716635e03eef49ab00c8dd35cf16a5674a27452968b16ba7756ba252b97be
4
+ data.tar.gz: 4bd6c34343347a1d705cea410346028bbb91446bf5983e75409f16c7730f1000
5
5
  SHA512:
6
- metadata.gz: 0fd8c7af3f87ce61121a7404b66cfae5cf78fea8bdb885dc7715467bb5092bd974a9fd4d7f94824fe10675d56b4e4dc7b439826f95bc5d9672f14fcb74692501
7
- data.tar.gz: 198328e45920aa2794cab2984e090c14c81ce56b4b038e4ae0b7f22ad66d327d0bbd34b5726709108c14609324dba1687a83a2f67026a2d8abf12d749067ca27
6
+ metadata.gz: f8b6c0c4ff6eabbc9a842cc863d76128dbccca74fabfef02b4aea6ba54f3a01ffd3d97447df4ccc027a8f22cf6a7b430b0893e34d0de7989e742442cf56b5ef9
7
+ data.tar.gz: 2404be74308d1b80c5d8f775f49f0f0ac6ae7deab47fe7c38e25e79852aac42f87e16b5cea5e4a4617cdde6eac1236cc8407a0e10fe2c9198bcfd0d6e88a7548
data/README.md CHANGED
@@ -85,6 +85,11 @@ The following platforms are supported:
85
85
  - OSX
86
86
  - RedHat
87
87
  - Ubuntu
88
+ - AlmaLinux
89
+
90
+ ## Limitations
91
+
92
+ - When PowerShell [Script Block Logging](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_logging_windows?view=powershell-7.4#enabling-script-block-logging) is enabled, data marked as sensitive in your manifest may appear in these logs as plain text. It is **highly recommended**, by both Puppet and Microsoft, that you also enable [Protected Event Logging](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_logging_windows?view=powershell-7.4#protected-event-logging) alongside this to encrypt the logs to protect this information.
88
93
 
89
94
  ## License
90
95
 
@@ -15,6 +15,7 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
15
15
  @cached_query_results = []
16
16
  @cached_test_results = []
17
17
  @logon_failures = []
18
+ @timeout = nil # default timeout, ps_manager.execute is expecting nil by default..
18
19
  super
19
20
  end
20
21
 
@@ -251,18 +252,22 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
251
252
  context.err('Logon credentials are invalid')
252
253
  return nil
253
254
  end
255
+ specify_dsc_timeout(name_hash)
254
256
  resource = invocable_resource(props, context, method)
255
257
  script_content = ps_script_content(resource)
258
+ context.debug("Invoke-DSC Timeout: #{@timeout} milliseconds") if @timeout
256
259
  context.debug("Script:\n #{redact_secrets(script_content)}")
257
- output = ps_manager.execute(remove_secret_identifiers(script_content))[:stdout]
260
+ output = ps_manager.execute(remove_secret_identifiers(script_content), @timeout)
258
261
 
259
- if output.nil?
260
- context.err('Nothing returned')
262
+ if output[:stdout].nil?
263
+ message = 'Nothing returned.'
264
+ message += " #{output[:errormessage]}" if output[:errormessage]&.match?(/PowerShell module timeout \(\d+ ms\) exceeded while executing/)
265
+ context.err(message)
261
266
  return nil
262
267
  end
263
268
 
264
269
  begin
265
- data = JSON.parse(output)
270
+ data = JSON.parse(output[:stdout])
266
271
  rescue StandardError => e
267
272
  context.err(e)
268
273
  return nil
@@ -295,6 +300,18 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
295
300
  data
296
301
  end
297
302
 
303
+ # Sets the @timeout instance variable.
304
+ # @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
305
+ # The @timeout variable is set to the value of name_hash[:dsc_timeout] in milliseconds
306
+ # If name_hash[:dsc_timeout] is nil, @timeout is not changed.
307
+ # If @timeout is already set to a value other than nil,
308
+ # it is changed only if it's different from name_hash[:dsc_timeout]..
309
+ def specify_dsc_timeout(name_hash)
310
+ return unless name_hash[:dsc_timeout] && (@timeout.nil? || @timeout != name_hash[:dsc_timeout])
311
+
312
+ @timeout = name_hash[:dsc_timeout] * 1000
313
+ end
314
+
298
315
  # Retries Invoke-DscResource when returned error matches error regex supplied as param.
299
316
  # @param context [Object] the Puppet runtime context to operate in and send feedback to
300
317
  # @param max_retry_count [Int] max number of times to retry Invoke-DscResource
@@ -1076,6 +1093,10 @@ class Puppet::Provider::DscBaseProvider # rubocop:disable Metrics/ClassLength
1076
1093
  def ps_manager
1077
1094
  debug_output = Puppet::Util::Log.level == :debug
1078
1095
  # TODO: Allow you to specify an alternate path, either to pwsh generally or a specific pwsh path.
1079
- Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
1096
+ if Pwsh::Util.on_windows?
1097
+ Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
1098
+ else
1099
+ Pwsh::Manager.instance(Pwsh::Manager.pwsh_path, Pwsh::Manager.pwsh_args, debug: debug_output)
1100
+ end
1080
1101
  end
1081
1102
  end
data/lib/pwsh/util.rb CHANGED
@@ -7,28 +7,24 @@ module Pwsh
7
7
  module_function
8
8
 
9
9
  # Verifies whether or not the current context is running on a Windows node.
10
+ # Implementation copied from `facets`: https://github.com/rubyworks/facets/blob/main/lib/standard/facets/rbconfig.rb
10
11
  #
11
12
  # @return [Bool] true if on windows
12
13
  def on_windows?
13
- # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
14
- # library uses that to test what platform it's on.
15
- !!File::ALT_SEPARATOR
14
+ host_os = RbConfig::CONFIG['host_os']
15
+ !!(host_os =~ /mswin|mingw/)
16
16
  end
17
17
 
18
- # Verify paths specified are valid directories which exist.
19
- #
20
- # @return [Bool] true if any directories specified do not exist
18
+ # Verify paths specified are valid directories.
19
+ # Skips paths which do not exist.
20
+ # @return [Bool] true if any paths specified are not valid directories
21
21
  def invalid_directories?(path_collection)
22
- invalid_paths = false
23
-
24
- return invalid_paths if path_collection.nil? || path_collection.empty?
22
+ return false if path_collection.nil? || path_collection.empty?
25
23
 
26
- paths = on_windows? ? path_collection.split(';') : path_collection.split(':')
27
- paths.each do |path|
28
- invalid_paths = true unless File.directory?(path) || path.empty?
29
- end
24
+ delimiter = on_windows? ? ';' : ':'
25
+ paths = path_collection.split(delimiter)
30
26
 
31
- invalid_paths
27
+ paths.any? { |path| !path.empty? && File.exist?(path) && !File.directory?(path) }
32
28
  end
33
29
 
34
30
  # Return a string or symbol converted to snake_case
data/lib/pwsh/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Pwsh
4
4
  # The version of the ruby-pwsh gem
5
- VERSION = '1.1.1'
5
+ VERSION = '1.2.1'
6
6
  end
data/lib/pwsh.rb CHANGED
@@ -108,7 +108,7 @@ module Pwsh
108
108
  @powershell_command = cmd
109
109
  @powershell_arguments = args
110
110
 
111
- raise "Bad configuration for ENV['lib']=#{ENV['lib']} - invalid path" if Pwsh::Util.invalid_directories?(ENV['lib'])
111
+ warn "Bad configuration for ENV['lib']=#{ENV['lib']} - invalid path" if Pwsh::Util.invalid_directories?(ENV['lib'])
112
112
 
113
113
  if Pwsh::Util.on_windows?
114
114
  # Named pipes under Windows will automatically be mounted in \\.\pipe\...
@@ -380,7 +380,7 @@ module Pwsh
380
380
  pwsh_paths << File.join(path, 'pwsh.exe') if File.exist?(File.join(path, 'pwsh.exe'))
381
381
  end
382
382
  else
383
- search_paths.split(File::PATH_SEPARATOR).each do |path|
383
+ search_paths.split(':').each do |path|
384
384
  pwsh_paths << File.join(path, 'pwsh') if File.exist?(File.join(path, 'pwsh'))
385
385
  end
386
386
  end
@@ -541,7 +541,7 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
541
541
 
542
542
  context 'when the invocation script returns data without errors' do
543
543
  before do
544
- allow(ps_manager).to receive(:execute).with(script).and_return({ stdout: 'DSC Data' })
544
+ allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' })
545
545
  allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data)
546
546
  allow(Puppet::Pops::Time::Timestamp).to receive(:parse).with('2100-01-01').and_return('TimeStamp:2100-01-01')
547
547
  allow(provider).to receive(:fetch_cached_hashes).and_return([])
@@ -659,15 +659,15 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
659
659
  context 'when the DSC invocation errors' do
660
660
  it 'writes an error and returns nil' do
661
661
  expect(provider).not_to receive(:logon_failed_already?)
662
- expect(ps_manager).to receive(:execute).with(script).and_return({ stdout: nil })
663
- expect(context).to receive(:err).with('Nothing returned')
662
+ expect(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: nil })
663
+ expect(context).to receive(:err).with('Nothing returned.')
664
664
  expect(result).to be_nil
665
665
  end
666
666
  end
667
667
 
668
668
  context 'when handling DateTimes' do
669
669
  before do
670
- allow(ps_manager).to receive(:execute).with(script).and_return({ stdout: 'DSC Data' })
670
+ allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' })
671
671
  allow(JSON).to receive(:parse).with('DSC Data').and_return(parsed_invocation_data)
672
672
  allow(provider).to receive(:fetch_cached_hashes).and_return([])
673
673
  end
@@ -719,7 +719,7 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
719
719
  context 'when the credential is invalid' do
720
720
  before do
721
721
  allow(provider).to receive(:logon_failed_already?).and_return(false)
722
- allow(ps_manager).to receive(:execute).with(script).and_return({ stdout: 'DSC Data' })
722
+ allow(ps_manager).to receive(:execute).with(script, nil).and_return({ stdout: 'DSC Data' })
723
723
  allow(JSON).to receive(:parse).with('DSC Data').and_return({ 'errormessage' => dsc_logon_failure_error })
724
724
  allow(context).to receive(:err).with(name_hash[:name], puppet_logon_failure_error)
725
725
  end
@@ -783,7 +783,7 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
783
783
  context 'when the invocation script returns nil' do
784
784
  it 'errors via context but does not raise' do
785
785
  expect(ps_manager).to receive(:execute).and_return({ stdout: nil })
786
- expect(context).to receive(:err).with('Nothing returned')
786
+ expect(context).to receive(:err).with('Nothing returned.')
787
787
  expect { result }.not_to raise_error
788
788
  end
789
789
  end
@@ -835,9 +835,29 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
835
835
  end
836
836
  end
837
837
 
838
+ context 'when a dsc_timeout is specified' do
839
+ let(:should_hash) { name.merge(dsc_timeout: 5) }
840
+ let(:apply_props_with_timeout) { { dsc_name: 'foo', dsc_timeout: 5 } }
841
+ let(:resource_with_timeout) { "Resource: #{apply_props_with_timeout}" }
842
+ let(:script_with_timeout) { "Script: #{apply_props_with_timeout}" }
843
+
844
+ before do
845
+ allow(provider).to receive(:invocable_resource).with(apply_props_with_timeout, context, 'set').and_return(resource_with_timeout)
846
+ allow(provider).to receive(:ps_script_content).with(resource_with_timeout).and_return(script_with_timeout)
847
+ allow(provider).to receive(:remove_secret_identifiers).with(script_with_timeout).and_return(script_with_timeout)
848
+ end
849
+
850
+ it 'sets @timeout and passes it to ps_manager.execute' do
851
+ provider.instance_variable_set(:@timeout, nil)
852
+ expect(ps_manager).to receive(:execute).with(script_with_timeout, 5000).and_return({ stdout: '{"in_desired_state": true, "errormessage": null}' })
853
+ provider.invoke_set_method(context, name, should_hash)
854
+ expect(provider.instance_variable_get(:@timeout)).to eq(5000)
855
+ end
856
+ end
857
+
838
858
  context 'when the invocation script returns data without errors' do
839
859
  it 'filters for the correct properties to invoke and returns the results' do
840
- expect(ps_manager).to receive(:execute).with("Script: #{apply_props}").and_return({ stdout: '{"in_desired_state": true, "errormessage": null}' })
860
+ expect(ps_manager).to receive(:execute).with("Script: #{apply_props}", nil).and_return({ stdout: '{"in_desired_state": true, "errormessage": null}' })
841
861
  expect(context).not_to receive(:err)
842
862
  expect(result).to eq({ 'in_desired_state' => true, 'errormessage' => nil })
843
863
  end
@@ -2110,21 +2130,44 @@ RSpec.describe Puppet::Provider::DscBaseProvider do
2110
2130
  end
2111
2131
 
2112
2132
  describe '.ps_manager' do
2113
- before do
2114
- allow(Pwsh::Manager).to receive(:powershell_path).and_return('pwsh')
2115
- allow(Pwsh::Manager).to receive(:powershell_args).and_return('args')
2116
- end
2133
+ describe '.ps_manager on non-Windows' do
2134
+ before do
2135
+ allow(Pwsh::Util).to receive(:on_windows?).and_return(false)
2136
+ allow(Pwsh::Manager).to receive(:pwsh_path).and_return('pwsh')
2137
+ allow(Pwsh::Manager).to receive(:pwsh_args).and_return('args')
2138
+ end
2139
+
2140
+ it 'Initializes an instance of the Pwsh::Manager' do
2141
+ expect(Puppet::Util::Log).to receive(:level).and_return(:normal)
2142
+ expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: false)
2143
+ expect { provider.ps_manager }.not_to raise_error
2144
+ end
2117
2145
 
2118
- it 'Initializes an instance of the Pwsh::Manager' do
2119
- expect(Puppet::Util::Log).to receive(:level).and_return(:normal)
2120
- expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: false)
2121
- expect { provider.ps_manager }.not_to raise_error
2146
+ it 'passes debug as true if Puppet::Util::Log.level is debug' do
2147
+ expect(Puppet::Util::Log).to receive(:level).and_return(:debug)
2148
+ expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: true)
2149
+ expect { provider.ps_manager }.not_to raise_error
2150
+ end
2122
2151
  end
2123
2152
 
2124
- it 'passes debug as true if Puppet::Util::Log.level is debug' do
2125
- expect(Puppet::Util::Log).to receive(:level).and_return(:debug)
2126
- expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: true)
2127
- expect { provider.ps_manager }.not_to raise_error
2153
+ describe '.ps_manager on Windows' do
2154
+ before do
2155
+ allow(Pwsh::Util).to receive(:on_windows?).and_return(true)
2156
+ allow(Pwsh::Manager).to receive(:powershell_path).and_return('pwsh')
2157
+ allow(Pwsh::Manager).to receive(:powershell_args).and_return('args')
2158
+ end
2159
+
2160
+ it 'Initializes an instance of the Pwsh::Manager' do
2161
+ expect(Puppet::Util::Log).to receive(:level).and_return(:normal)
2162
+ expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: false)
2163
+ expect { provider.ps_manager }.not_to raise_error
2164
+ end
2165
+
2166
+ it 'passes debug as true if Puppet::Util::Log.level is debug' do
2167
+ expect(Puppet::Util::Log).to receive(:level).and_return(:debug)
2168
+ expect(Pwsh::Manager).to receive(:instance).with('pwsh', 'args', debug: true)
2169
+ expect { provider.ps_manager }.not_to raise_error
2170
+ end
2128
2171
  end
2129
2172
  end
2130
2173
  end
@@ -87,13 +87,15 @@ RSpec.describe Pwsh::Util do
87
87
  end
88
88
 
89
89
  describe '.invalid_directories?' do
90
- let(:valid_path_a) { 'C:/some/folder' }
91
- let(:valid_path_b) { 'C:/another/folder' }
92
- let(:valid_paths) { 'C:/some/folder;C:/another/folder' }
93
- let(:invalid_path) { 'C:/invalid/path' }
94
- let(:mixed_paths) { 'C:/some/folder;C:/invalid/path;C:/another/folder' }
95
- let(:empty_string) { '' }
96
- let(:empty_members) { 'C:/some/folder;;C:/another/folder' }
90
+ let(:valid_path_a) { 'C:/some/folder' }
91
+ let(:valid_path_b) { 'C:/another/folder' }
92
+ let(:valid_paths) { 'C:/some/folder;C:/another/folder' }
93
+ let(:invalid_path) { 'C:/invalid/path' }
94
+ let(:mixed_paths) { 'C:/some/folder;C:/another/folder;C:/invalid/path' }
95
+ let(:empty_string) { '' }
96
+ let(:file_path) { 'C:/some/folder/file.txt' }
97
+ let(:non_existent_dir) { 'C:/some/dir/that/doesnt/exist' }
98
+ let(:empty_members) { 'C:/some/folder;;C:/another/folder' }
97
99
 
98
100
  it 'returns false if passed nil' do
99
101
  expect(described_class.invalid_directories?(nil)).to be false
@@ -103,8 +105,16 @@ RSpec.describe Pwsh::Util do
103
105
  expect(described_class.invalid_directories?('')).to be false
104
106
  end
105
107
 
108
+ it 'returns true if a file path is provided' do
109
+ expect(described_class).to receive(:on_windows?).and_return(true)
110
+ expect(File).to receive(:exist?).with(file_path).and_return(true)
111
+ expect(File).to receive(:directory?).with(file_path).and_return(false)
112
+ expect(described_class.invalid_directories?(file_path)).to be true
113
+ end
114
+
106
115
  it 'returns false if one valid path is provided' do
107
116
  expect(described_class).to receive(:on_windows?).and_return(true)
117
+ expect(File).to receive(:exist?).with(valid_path_a).and_return(true)
108
118
  expect(File).to receive(:directory?).with(valid_path_a).and_return(true)
109
119
  expect(described_class.invalid_directories?(valid_path_a)).to be false
110
120
  end
@@ -112,31 +122,56 @@ RSpec.describe Pwsh::Util do
112
122
  it 'returns false if a collection of valid paths is provided' do
113
123
  expect(described_class).to receive(:on_windows?).and_return(true)
114
124
  expect(File).to receive(:directory?).with(valid_path_a).and_return(true)
125
+ expect(File).to receive(:exist?).with(valid_path_a).and_return(true)
115
126
  expect(File).to receive(:directory?).with(valid_path_b).and_return(true)
127
+ expect(File).to receive(:exist?).with(valid_path_b).and_return(true)
116
128
  expect(described_class.invalid_directories?(valid_paths)).to be false
117
129
  end
118
130
 
119
131
  it 'returns true if there is only one path and it is invalid' do
120
132
  expect(described_class).to receive(:on_windows?).and_return(true)
133
+ expect(File).to receive(:exist?).with(invalid_path).and_return(true)
121
134
  expect(File).to receive(:directory?).with(invalid_path).and_return(false)
122
135
  expect(described_class.invalid_directories?(invalid_path)).to be true
123
136
  end
124
137
 
125
138
  it 'returns true if the collection has on valid and one invalid member' do
126
139
  expect(described_class).to receive(:on_windows?).and_return(true)
140
+ expect(File).to receive(:exist?).with(valid_path_a).and_return(true)
127
141
  expect(File).to receive(:directory?).with(valid_path_a).and_return(true)
142
+ expect(File).to receive(:exist?).with(valid_path_b).and_return(true)
128
143
  expect(File).to receive(:directory?).with(valid_path_b).and_return(true)
144
+ expect(File).to receive(:exist?).with(invalid_path).and_return(true)
129
145
  expect(File).to receive(:directory?).with(invalid_path).and_return(false)
130
146
  expect(described_class.invalid_directories?(mixed_paths)).to be true
131
147
  end
132
148
 
133
149
  it 'returns false if collection has empty members but other entries are valid' do
134
150
  expect(described_class).to receive(:on_windows?).and_return(true)
151
+ expect(File).to receive(:exist?).with(valid_path_a).and_return(true)
135
152
  expect(File).to receive(:directory?).with(valid_path_a).and_return(true)
153
+ expect(File).to receive(:exist?).with(valid_path_b).and_return(true)
136
154
  expect(File).to receive(:directory?).with(valid_path_b).and_return(true)
137
155
  allow(File).to receive(:directory?).with('')
138
156
  expect(described_class.invalid_directories?(empty_members)).to be false
139
157
  end
158
+
159
+ it 'returns true if a collection has valid members but also contains a file path' do
160
+ expect(described_class).to receive(:on_windows?).and_return(true)
161
+ expect(File).to receive(:exist?).with(valid_path_a).and_return(true)
162
+ expect(File).to receive(:directory?).with(valid_path_a).and_return(true)
163
+ expect(File).to receive(:exist?).with(file_path).and_return(true)
164
+ expect(File).to receive(:directory?).with(file_path).and_return(false)
165
+ expect(described_class.invalid_directories?("#{valid_path_a};#{file_path}")).to be true
166
+ end
167
+
168
+ it 'returns false if a collection has valid members but contains a non-existent dir path' do
169
+ expect(described_class).to receive(:on_windows?).and_return(true)
170
+ expect(File).to receive(:exist?).with(valid_path_a).and_return(true)
171
+ expect(File).to receive(:directory?).with(valid_path_a).and_return(true)
172
+ expect(File).to receive(:exist?).with(non_existent_dir).and_return(false)
173
+ expect(described_class.invalid_directories?("#{valid_path_a};#{non_existent_dir}")).to be false
174
+ end
140
175
  end
141
176
 
142
177
  describe '.snake_case' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-pwsh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-21 00:00:00.000000000 Z
11
+ date: 2024-09-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: PowerShell code manager for ruby.
14
14
  email: