ruby-pwsh 1.1.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
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: