ruby-pwsh 0.4.1 → 0.5.0

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: 2fcc279675091bc4fcf7b31a7f309561c49b14352895424f72ca9170ffd51de3
4
- data.tar.gz: 2c1160616d59bc30f1a641469617e0b941e610bf48e8c640a1de2a95df66778c
3
+ metadata.gz: 83a03013a6f1fd8f72b90d29d76f211cf1fbba6f32a8e5275bc7b9ce29d7a52a
4
+ data.tar.gz: 8ce907ddb3d833eec4a193ce994e05a990ccc2f20e46518674896efa14bd6328
5
5
  SHA512:
6
- metadata.gz: f957369ca4ed3b5932c843195bd9ec20614dd7211b8f0fd1e1ed76833aa8912d897a5341d605c89b95e7b76a9ba623c3064709e2ed1182415dbb907285d61dbd
7
- data.tar.gz: 7508304850d8663c31f8ce420accf3a3007e594176365199ddb910e50a383ab7dc2b7e17911213c3fc5cea66eafcfb4ae89b7554f03b47df94fcd81483cad056
6
+ metadata.gz: 71abf1b0cc94d5f742ae6240d08fff15f86e588157a4a482501f2383ac11e5f3644fb43702af5071720193303fa803debeed362bb20f2efec205b0137acb115e
7
+ data.tar.gz: b9c9ce3518e6cbc27c6b0f8126519a1c1ea53158424f72162b75e23ecd28415e80cf5df4e9dc762aadd93a99e30e16a5a91dc405cd9d8571a53184bbe04e4373
@@ -1,3 +1,5 @@
1
+ Gemspec/RequiredRubyVersion:
2
+ Enabled: false
1
3
  Layout/EndOfLine:
2
4
  Description: Don't enforce CRLF on Windows.
3
5
  Enabled: false
@@ -28,4 +30,12 @@ Naming/FileName:
28
30
  Style/RescueStandardError:
29
31
  Enabled: false
30
32
  Style/ExpandPathArguments:
31
- Enabled: false
33
+ Enabled: false
34
+ Style/Documentation:
35
+ Enabled: false
36
+ Style/ClassAndModuleChildren:
37
+ Exclude:
38
+ - lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb
39
+ Style/ClassVars:
40
+ Exclude:
41
+ - lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb
@@ -19,6 +19,7 @@ script:
19
19
  - 'bundle exec rake $CHECK'
20
20
  rvm:
21
21
  - 2.5.1
22
+ - 2.7
22
23
  matrix:
23
24
  include:
24
25
  - env: CHECK="rubocop"
@@ -2,7 +2,15 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
4
4
 
5
- ## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-12)
5
+ ## [0.5.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.0) (2020-08-20)
6
+
7
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.1...0.5.0)
8
+
9
+ ### Added
10
+
11
+ - \(IAC-1045\) Add the DSC base Puppet provider to pwshlib [\#39](https://github.com/puppetlabs/ruby-pwsh/pull/39) ([michaeltlombardi](https://github.com/michaeltlombardi))
12
+
13
+ ## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-13)
6
14
 
7
15
  [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.0...0.4.1)
8
16
 
@@ -16,7 +24,7 @@ All notable changes to this project will be documented in this file.The format i
16
24
 
17
25
  ### Added
18
26
 
19
- - \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([cmccrisken-puppet](https://github.com/cmccrisken-puppet))
27
+ - \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
20
28
 
21
29
  ## [0.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-04)
22
30
 
@@ -41,4 +49,4 @@ All notable changes to this project will be documented in this file.The format i
41
49
 
42
50
 
43
51
 
44
- \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
52
+ \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
data/Gemfile CHANGED
@@ -17,25 +17,20 @@ group :test do
17
17
  end
18
18
 
19
19
  group :development do
20
- # TODO: Use gem instead of git. Section mapping is merged into master, but not yet released
21
- gem 'github_changelog_generator', git: 'https://github.com/skywinder/github-changelog-generator.git', ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018'
20
+ gem 'github_changelog_generator', '~> 1.15' if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.3.0')
22
21
  gem 'yard'
23
22
  end
24
23
 
25
24
  group :puppet do
26
25
  gem 'pdk', '~> 1.0'
26
+ gem 'puppet'
27
27
  end
28
28
 
29
29
  group :pry do
30
30
  gem 'fuubar'
31
31
 
32
- if RUBY_VERSION == '1.8.7'
33
- gem 'debugger'
34
- elsif RUBY_VERSION =~ /^2\.[01]/
35
- gem 'byebug', '~> 9.0.0'
32
+ if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.4.0')
36
33
  gem 'pry-byebug'
37
- elsif RUBY_VERSION =~ /^2\.[23456789]/
38
- gem 'pry-byebug' # rubocop:disable Bundler/DuplicatedGem
39
34
  else
40
35
  gem 'pry-debugger'
41
36
  end
data/README.md CHANGED
@@ -70,4 +70,50 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
70
70
 
71
71
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). -->
72
72
 
73
+ ## Releasing the Gem and Puppet Module
74
+
75
+ Steps to release an update to the gem and module include:
76
+
77
+ 1. Ensure that the release branch is up to date with the master:
78
+ ```bash
79
+ git push upstream upstream/master:release --force
80
+ ```
81
+ 1. Checkout a new working branch for the release prep (where xyz is the appropriate version, sans periods):
82
+ ```bash
83
+ git checkout -b maint/release/prep-xyz upstream/release
84
+ ```
85
+ 1. Update the version in `lib/pwsh/version.rb` and `metadata.json` to the appropriate version for the new release.
86
+ 1. Run the changelog update task (make sure to verify the changelog, correctly tagging PRs as needed):
87
+ ```bash
88
+ bundle exec rake changelog
89
+ ```
90
+ 1. Commit your changes with a short, sensible commit message, like:
91
+ ```text
92
+ git add lib/pwsh/version.rb
93
+ git add metadata.json
94
+ git add CHANGELOG.md
95
+ git commit -m '(MAINT) Prep for x.y.z release'
96
+ ```
97
+ 1. Push your changes and submit a pull request for review _against the **release** branch_:
98
+ ```bash
99
+ git push -u origin maint/release-prep-xyz
100
+ ```
101
+ 1. Ensure tests pass and the code is merged to `release`.
102
+ 1. Grab the commit hash from the merge commit on release, use that as the tag for the version (replacing `x.y.z` with the appropriate version and `commithash` with the relevant one), then push the tags to upstream:
103
+ ```bash
104
+ bundle exec rake tag['x.y.z', 'commithash']
105
+ ```
106
+ 1. Build the Ruby gem and publish:
107
+ ```bash
108
+ bundle exec rake build
109
+ bundle exec rake push['ruby-pwsh-x.y.z.gem']
110
+ ```
111
+ 1. Verify that the correct version now exists on [RubyGems](https://rubygems.org/search?query=ruby-pwsh)
112
+ 1. Build the Puppet module:
113
+ ```bash
114
+ bundle exec rake build_module
115
+ ```
116
+ 1. Publish the updated module version (found in the `pkg` folder) to [the Forge](https://forge.puppet.com/puppetlabs/pwshlib).
117
+ 1. Submit the [mergeback PR from the release branch to master](https://github.com/puppetlabs/ruby-pwsh/compare/master...release).
118
+
73
119
  ## Known Issues
@@ -0,0 +1,38 @@
1
+ ---
2
+ version: 1.1.x.{build}
3
+ branches:
4
+ only:
5
+ - master
6
+ - release
7
+ clone_depth: 10
8
+ environment:
9
+ matrix:
10
+ -
11
+ RUBY_VERSION: 25-x64
12
+ CHECK: rubocop
13
+ -
14
+ RUBY_VERSION: 25
15
+ CHECK: spec
16
+ COVERAGE: yes
17
+ matrix:
18
+ fast_finish: true
19
+ install:
20
+ - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH%
21
+ - bundle install --jobs 4 --retry 2
22
+ - type Gemfile.lock
23
+ build: off
24
+ build_script:
25
+ - dir .
26
+ test_script:
27
+ - ruby -v
28
+ - gem -v
29
+ - bundle -v
30
+ - pwsh -v
31
+ - bundle exec rake %CHECK%
32
+ notifications:
33
+ - provider: Email
34
+ to:
35
+ - nobody@nowhere.com
36
+ on_build_success: false
37
+ on_build_failure: false
38
+ on_build_status_changed: false
@@ -0,0 +1,666 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet/resource_api/simple_provider'
4
+ require 'securerandom'
5
+ require 'ruby-pwsh'
6
+ require 'pathname'
7
+ require 'json'
8
+
9
+ class Puppet::Provider::DscBaseProvider < Puppet::ResourceApi::SimpleProvider
10
+ # Initializes the provider, preparing the class variables which cache:
11
+ # - the canonicalized resources across calls
12
+ # - query results
13
+ # - logon failures
14
+ def initialize
15
+ @@cached_canonicalized_resource = []
16
+ @@cached_query_results = []
17
+ @@logon_failures = []
18
+ end
19
+
20
+ # Look through a cache to retrieve the hashes specified, if they have been cached.
21
+ # Does so by seeing if each of the specified hashes is a subset of any of the hashes
22
+ # in the cache, so {foo: 1, bar: 2} would return if {foo: 1} was the search hash.
23
+ #
24
+ # @param cache [Array] the class variable containing cached hashes to search through
25
+ # @param hashes [Array] the list of hashes to search the cache for
26
+ # @return [Array] an array containing the matching hashes for the search condition, if any
27
+ def fetch_cached_hashes(cache, hashes)
28
+ cache.reject do |item|
29
+ matching_hash = hashes.select { |hash| (item.to_a - hash.to_a).empty? || (hash.to_a - item.to_a).empty? }
30
+ matching_hash.empty?
31
+ end.flatten
32
+ end
33
+
34
+ # Implements the canonicalize feature of the Resource API; this method is called first against any resources
35
+ # defined in the manifest, then again to conform the results from a get call. The method attempts to retrieve
36
+ # the DSC resource from the machine; if the resource is found, this method then compares the downcased values
37
+ # of the two hashes, overwriting the manifest value with the discovered one if they are case insensitively
38
+ # equivalent; this enables case insensitive but preserving behavior where a manifest declaration of a path as
39
+ # "c:/foo/bar" if discovered on disk as "C:\Foo\Bar" will canonicalize to the latter and prevent any flapping.
40
+ #
41
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
42
+ # @param resources [Hash] the hash of the resource to canonicalize from either manifest or invocation
43
+ # @return [Hash] returns a hash representing the current state of the object, if it exists
44
+ def canonicalize(context, resources)
45
+ canonicalized_resources = []
46
+ resources.collect do |r|
47
+ if fetch_cached_hashes(@@cached_canonicalized_resource, [r]).empty?
48
+ canonicalized = invoke_get_method(context, r)
49
+ if canonicalized.nil?
50
+ canonicalized = r.dup
51
+ @@cached_canonicalized_resource << r.dup
52
+ else
53
+ canonicalized[:name] = r[:name]
54
+ if r[:dsc_psdscrunascredential].nil?
55
+ canonicalized.delete(:dsc_psdscrunascredential)
56
+ else
57
+ canonicalized[:dsc_psdscrunascredential] = r[:dsc_psdscrunascredential]
58
+ end
59
+ downcased_result = recursively_downcase(canonicalized)
60
+ downcased_resource = recursively_downcase(r)
61
+ downcased_result.each do |key, value|
62
+ canonicalized[key] = r[key] unless downcased_resource[key] == value
63
+ canonicalized.delete(key) unless downcased_resource.keys.include?(key)
64
+ end
65
+ # Cache the actually canonicalized resource separately
66
+ @@cached_canonicalized_resource << canonicalized.dup
67
+ end
68
+ else
69
+ canonicalized = r
70
+ end
71
+ canonicalized_resources << canonicalized
72
+ end
73
+ context.debug("Canonicalized Resources: #{canonicalized_resources}")
74
+ canonicalized_resources
75
+ end
76
+
77
+ # Attempts to retrieve an instance of the DSC resource, invoking the `Get` method and passing any
78
+ # namevars as the Properties to Invoke-DscResource. The result object, if any, is compared to the
79
+ # specified properties in the Puppet Resource to decide whether it needs to be created, updated,
80
+ # deleted, or whether it is in the desired state.
81
+ #
82
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
83
+ # @param names [Hash] the hash of namevar properties and their values to use to get the resource
84
+ # @return [Hash] returns a hash representing the current state of the object, if it exists
85
+ def get(context, names = nil)
86
+ # Relies on the get_simple_filter feature to pass the namevars
87
+ # as an array containing the namevar parameters as a hash.
88
+ # This hash is functionally the same as a should hash as
89
+ # passed to the should_to_resource method.
90
+ context.debug('Collecting data from the DSC Resource')
91
+
92
+ # If the resource has already been queried, do not bother querying for it again
93
+ cached_results = fetch_cached_hashes(@@cached_query_results, names)
94
+ return cached_results unless cached_results.empty?
95
+
96
+ if @@cached_canonicalized_resource.empty?
97
+ mandatory_properties = {}
98
+ else
99
+ canonicalized_resource = @@cached_canonicalized_resource[0].dup
100
+ mandatory_properties = canonicalized_resource.select do |attribute, _value|
101
+ (mandatory_get_attributes(context) - namevar_attributes(context)).include?(attribute)
102
+ end
103
+ # If dsc_psdscrunascredential was specified, re-add it here.
104
+ mandatory_properties[:dsc_psdscrunascredential] = canonicalized_resource[:dsc_psdscrunascredential] if canonicalized_resource.keys.include?(:dsc_psdscrunascredential)
105
+ end
106
+ names.collect do |name|
107
+ name = { name: name } if name.is_a? String
108
+ invoke_get_method(context, name.merge(mandatory_properties))
109
+ end
110
+ end
111
+
112
+ # Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
113
+ # the `invoke_set_method` method; whether this method, `update`, or `delete` is called is entirely
114
+ # up to the Resource API based on the results
115
+ #
116
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
117
+ # @param name [String] the name of the resource being created
118
+ # @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
119
+ def create(context, name, should)
120
+ context.debug("Creating '#{name}' with #{should.inspect}")
121
+ invoke_set_method(context, name, should)
122
+ end
123
+
124
+ # Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
125
+ # the `invoke_set_method` method; whether this method, `create`, or `delete` is called is entirely
126
+ # up to the Resource API based on the results
127
+ #
128
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
129
+ # @param name [String] the name of the resource being created
130
+ # @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
131
+ def update(context, name, should)
132
+ context.debug("Updating '#{name}' with #{should.inspect}")
133
+ invoke_set_method(context, name, should)
134
+ end
135
+
136
+ # Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
137
+ # the `invoke_set_method` method; whether this method, `create`, or `update` is called is entirely
138
+ # up to the Resource API based on the results
139
+ #
140
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
141
+ # @param name [String] the name of the resource being created
142
+ # @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
143
+ def delete(context, name)
144
+ context.debug("Deleting '#{name}'")
145
+ invoke_set_method(context, name, name.merge({ dsc_ensure: 'Absent' }))
146
+ end
147
+
148
+ # Invokes the `Get` method, passing the name_hash as the properties to use with `Invoke-DscResource`
149
+ # The PowerShell script returns a JSON representation of the DSC Resource's CIM Instance munged as
150
+ # best it can be for Ruby. Once that JSON is parsed into a hash this method further munges it to
151
+ # fit the expected property definitions. Finally, it returns the object for the Resource API to
152
+ # compare against and determine what future actions, if any, are needed.
153
+ #
154
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
155
+ # @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
156
+ # @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects
157
+ def invoke_get_method(context, name_hash)
158
+ context.debug("retrieving #{name_hash.inspect}")
159
+
160
+ # Do not bother running if the logon credentials won't work
161
+ unless name_hash[:dsc_psdscrunascredential].nil?
162
+ return name_hash if logon_failed_already?(name_hash[:dsc_psdscrunascredential])
163
+ end
164
+
165
+ query_props = name_hash.select { |k, v| mandatory_get_attributes(context).include?(k) || (k == :dsc_psdscrunascredential && !v.nil?) }
166
+ resource = should_to_resource(query_props, context, 'get')
167
+ script_content = ps_script_content(resource)
168
+ context.debug("Script:\n #{redact_secrets(script_content)}")
169
+ output = ps_manager.execute(script_content)[:stdout]
170
+ context.err('Nothing returned') if output.nil?
171
+
172
+ data = JSON.parse(output)
173
+ context.debug("raw data received: #{data.inspect}")
174
+ error = data['errormessage']
175
+ unless error.nil?
176
+ # NB: We should have a way to stop processing this resource *now* without blowing up the whole Puppet run
177
+ # Raising an error stops processing but blows things up while context.err alerts but continues to process
178
+ if error =~ /Logon failure: the user has not been granted the requested logon type at this computer/
179
+ logon_error = "PSDscRunAsCredential account specified (#{name_hash[:dsc_psdscrunascredential]['user']}) does not have appropriate logon rights; are they an administrator?"
180
+ name_hash[:name].nil? ? context.err(logon_error) : context.err(name_hash[:name], logon_error)
181
+ @@logon_failures << name_hash[:dsc_psdscrunascredential].dup
182
+ # This is a hack to handle the query cache to prevent a second lookup
183
+ @@cached_query_results << name_hash # if fetch_cached_hashes(@@cached_query_results, [data]).empty?
184
+ else
185
+ context.err(error)
186
+ end
187
+ # Either way, something went wrong and we didn't get back a good result, so return nil
188
+ return nil
189
+ end
190
+ # DSC gives back information we don't care about; filter down to only
191
+ # those properties exposed in the type definition.
192
+ valid_attributes = context.type.attributes.keys.collect(&:to_s)
193
+ data.select! { |key, _value| valid_attributes.include?("dsc_#{key.downcase}") }
194
+ # Canonicalize the results to match the type definition representation;
195
+ # failure to do so will prevent the resource_api from comparing the result
196
+ # to the should hash retrieved from the resource definition in the manifest.
197
+ data.keys.each do |key|
198
+ type_key = "dsc_#{key.downcase}".to_sym
199
+ data[type_key] = data.delete(key)
200
+ camelcase_hash_keys!(data[type_key]) if data[type_key].is_a?(Enumerable)
201
+ end
202
+ # If a resource is found, it's present, so refill these two Puppet-only keys
203
+ data.merge!({ name: name_hash[:name] })
204
+ if ensurable?(context)
205
+ ensure_value = data.key?(:dsc_ensure) ? data[:dsc_ensure].downcase : 'present'
206
+ data.merge!({ ensure: ensure_value })
207
+ end
208
+ # TODO: Handle PSDscRunAsCredential flapping
209
+ # Resources do not return the account under which they were discovered, so re-add that
210
+ if name_hash[:dsc_psdscrunascredential].nil?
211
+ data.delete(:dsc_psdscrunascredential)
212
+ else
213
+ data.merge!({ dsc_psdscrunascredential: name_hash[:dsc_psdscrunascredential] })
214
+ end
215
+ # Cache the query to prevent a second lookup
216
+ @@cached_query_results << data.dup if fetch_cached_hashes(@@cached_query_results, [data]).empty?
217
+ context.debug("Returned to Puppet as #{data}")
218
+ data
219
+ end
220
+
221
+ # Invokes the `Set` method, passing the should hash as the properties to use with `Invoke-DscResource`
222
+ # The PowerShell script returns a JSON hash with key-value pairs indicating whether or not the resource
223
+ # is in the desired state, whether or not it requires a reboot, and any error messages captured.
224
+ #
225
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
226
+ # @param should [Hash] the desired state represented definition to pass as properties to Invoke-DscResource
227
+ # @return [Hash] returns a hash indicating whether or not the resource is in the desired state, whether or not it requires a reboot, and any error messages captured.
228
+ def invoke_set_method(context, name, should)
229
+ context.debug("Invoking Set Method for '#{name}' with #{should.inspect}")
230
+
231
+ # Do not bother running if the logon credentials won't work
232
+ unless should[:dsc_psdscrunascredential].nil?
233
+ return nil if logon_failed_already?(should[:dsc_psdscrunascredential])
234
+ end
235
+
236
+ apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
237
+ resource = should_to_resource(apply_props, context, 'set')
238
+ script_content = ps_script_content(resource)
239
+ context.debug("Script:\n #{redact_secrets(script_content)}")
240
+
241
+ output = ps_manager.execute(script_content)[:stdout]
242
+ context.err('Nothing returned') if output.nil?
243
+
244
+ data = JSON.parse(output)
245
+ context.debug(data)
246
+
247
+ context.err(data['errormessage']) unless data['errormessage'].empty?
248
+ # TODO: Implement this functionality for notifying a DSC reboot?
249
+ # notify_reboot_pending if data['rebootrequired'] == true
250
+ data
251
+ end
252
+
253
+ # Converts a Puppet resource hash into a hash with the information needed to call Invoke-DscResource,
254
+ # including the desired state, the path to the PowerShell module containing the resources, the invoke
255
+ # method, and metadata about the DSC Resource and Puppet Type.
256
+ #
257
+ # @param should [Hash] A hash representing the desired state of the DSC resource as defined in Puppet
258
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
259
+ # @param dsc_invoke_method [String] the method to pass to Invoke-DscResource: get, set, or test
260
+ # @return [Hash] a hash with the information needed to run `Invoke-DscResource`
261
+ def should_to_resource(should, context, dsc_invoke_method)
262
+ resource = {}
263
+ resource[:parameters] = {}
264
+ %i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_module_name dscmeta_module_version].each do |k|
265
+ resource[k] = context.type.definition[k]
266
+ end
267
+ should.each do |k, v|
268
+ next if k == :ensure
269
+ # PSDscRunAsCredential is considered a namevar and will always be passed, even if nil
270
+ # To prevent flapping during runs, remove it from the resource definition unless specified
271
+ next if k == :dsc_psdscrunascredential && v.nil?
272
+
273
+ resource[:parameters][k] = {}
274
+ resource[:parameters][k][:value] = v
275
+ %i[mof_type mof_is_embedded].each do |ky|
276
+ resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky]
277
+ end
278
+ end
279
+ resource[:dsc_invoke_method] = dsc_invoke_method
280
+
281
+ # Because Puppet adds all of the modules to the LOAD_PATH we can be sure that the appropriate module lives here
282
+ # PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
283
+ root_module_path = $LOAD_PATH.select { |path| path.match?(%r{#{resource[:dscmeta_module_name].downcase}/lib}) }.first
284
+ resource[:vendored_modules_path] = File.expand_path(root_module_path + '/puppet_x/dsc_resources')
285
+ # resource[:vendored_modules_path] = File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources')
286
+ resource[:attributes] = nil
287
+ context.debug("should_to_resource: #{resource.inspect}")
288
+ resource
289
+ end
290
+
291
+ # Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique
292
+ # variables in the PowerShell script.
293
+ #
294
+ # @return [String] a uuid with underscores instead of dashes.
295
+ def random_variable_name
296
+ # PowerShell variables can't include dashes
297
+ SecureRandom.uuid.gsub('-', '_')
298
+ end
299
+
300
+ # Return a Hash containing all of the variables defined for instantiation as well as the Ruby hash for their
301
+ # properties so they can be matched and replaced as needed.
302
+ #
303
+ # @return [Hash] containing all instantiated variables and the properties that they define
304
+ def instantiated_variables
305
+ @@instantiated_variables ||= {}
306
+ end
307
+
308
+ # Clear the instantiated variables hash to be ready for the next run
309
+ def clear_instantiated_variables!
310
+ @@instantiated_variables = {}
311
+ end
312
+
313
+ # Return true if the specified credential hash has already failed to execute a DSC resource due to
314
+ # a logon error, as when the account is not an administrator on the machine; otherwise returns false.
315
+ #
316
+ # @param [Hash] a credential hash with a user and password keys where the password is a sensitive string
317
+ # @return [Bool] true if the credential_hash has already failed logon, false otherwise
318
+ def logon_failed_already?(credential_hash)
319
+ @@logon_failures.any? do |failure_hash|
320
+ failure_hash['user'] == credential_hash['user'] && failure_hash['password'].unwrap == credential_hash['password'].unwrap
321
+ end
322
+ end
323
+
324
+ # Recursively transforms any enumerable, camelCasing any hash keys it finds
325
+ #
326
+ # @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
327
+ # @return [Enumerable] returns the input object with hash keys recursively camelCased
328
+ def camelcase_hash_keys!(enumerable)
329
+ if enumerable.is_a?(Hash)
330
+ enumerable.keys.each do |key|
331
+ name = key.dup
332
+ name[0] = name[0].downcase
333
+ enumerable[name] = enumerable.delete(key)
334
+ camelcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
335
+ end
336
+ else
337
+ enumerable.each { |item| camelcase_hash_keys!(item) if item.is_a?(Enumerable) }
338
+ end
339
+ end
340
+
341
+ # Recursively transforms any object, downcasing it to enable case insensitive comparisons
342
+ #
343
+ # @param object [Object] a string, array, hash, or other object to attempt to recursively downcase
344
+ # @return [Object] returns the input object recursively downcased
345
+ def recursively_downcase(object)
346
+ case object
347
+ when String
348
+ object.downcase
349
+ when Array
350
+ object.map { |item| recursively_downcase(item) }
351
+ when Hash
352
+ transformed = {}
353
+ object.transform_keys(&:downcase).each do |key, value|
354
+ transformed[key] = recursively_downcase(value)
355
+ end
356
+ transformed
357
+ else
358
+ object
359
+ end
360
+ end
361
+
362
+ # Checks to see whether the DSC resource being managed is defined as ensurable
363
+ #
364
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
365
+ # @return [Bool] returns true if the DSC Resource is ensurable, otherwise false.
366
+ def ensurable?(context)
367
+ context.type.attributes.keys.include?(:ensure)
368
+ end
369
+
370
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
371
+ #
372
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
373
+ # @return [Array] returns an array of attribute names as symbols which are mandatory for get operations
374
+ def mandatory_get_attributes(context)
375
+ context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_get] }.keys
376
+ end
377
+
378
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for set operations
379
+ #
380
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
381
+ # @return [Array] returns an array of attribute names as symbols which are mandatory for set operations
382
+ def mandatory_set_attributes(context)
383
+ context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_set] }.keys
384
+ end
385
+
386
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as namevars
387
+ #
388
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
389
+ # @return [Array] returns an array of attribute names as symbols which are namevars
390
+ def namevar_attributes(context)
391
+ context.type.attributes.select { |_attribute, properties| properties[:behaviour] == :namevar }.keys
392
+ end
393
+
394
+ # Look through a fully formatted string, replacing all instances where a value matches the formatted properties
395
+ # of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
396
+ # CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
397
+ #
398
+ # @param string [String] the string of text to search through for places an instantiated variable can be referenced
399
+ # @return [String] the string with references to instantiated variables instead of their properties
400
+ def interpolate_variables(string)
401
+ modified_string = string
402
+ # Always replace later-created variables first as they sometimes were built from earlier ones
403
+ instantiated_variables.reverse_each do |variable_name, ruby_definition|
404
+ modified_string = modified_string.gsub(format(ruby_definition), "$#{variable_name}")
405
+ end
406
+ modified_string
407
+ end
408
+
409
+ # Parses a resource definition (as from `should_to_resource`) for any properties which are PowerShell
410
+ # Credentials. As these values need to be serialized into PSCredential objects, return an array of
411
+ # PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
412
+ # These credential variables can then be simply assigned in the parameter hash where needed.
413
+ #
414
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
415
+ # @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables
416
+ def prepare_credentials(resource)
417
+ credentials_block = []
418
+ resource[:parameters].each do |_property_name, property_hash|
419
+ next unless property_hash[:mof_type] == 'PSCredential'
420
+ next if property_hash[:value].nil?
421
+
422
+ variable_name = random_variable_name
423
+ credential_hash = {
424
+ 'user' => property_hash[:value]['user'],
425
+ 'password' => escape_quotes(property_hash[:value]['password'].unwrap)
426
+ }
427
+ instantiated_variables.merge!(variable_name => credential_hash)
428
+ credentials_block << format_pscredential(variable_name, credential_hash)
429
+ end
430
+ credentials_block.join("\n")
431
+ credentials_block == [] ? '' : credentials_block
432
+ end
433
+
434
+ # Write a line of PowerShell which creates a PSCredential object and assigns it to a variable
435
+ #
436
+ # @param variable_name [String] the name of the Variable to assign the PSCredential object to
437
+ # @param credential_hash [Hash] the Properties which define the PSCredential Object
438
+ # @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
439
+ def format_pscredential(variable_name, credential_hash)
440
+ definition = "$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}' # PuppetSensitive"
441
+ definition
442
+ end
443
+
444
+ # Parses a resource definition (as from `should_to_resource`) for any properties which are CIM instances
445
+ # whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
446
+ # those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
447
+ # will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
448
+ # by variable reference.
449
+ #
450
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
451
+ # @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables
452
+ def prepare_cim_instances(resource)
453
+ cim_instances_block = []
454
+ resource[:parameters].each do |_property_name, property_hash|
455
+ next unless property_hash[:mof_is_embedded]
456
+
457
+ # strip dsc_ from the beginning of the property name declaration
458
+ # name = property_name.to_s.gsub(/^dsc_/, '').to_sym
459
+ # Process nested CIM instances first as those neeed to be passed to higher-order
460
+ # instances and must therefore be declared before they must be referenced
461
+ cim_instance_hashes = nested_cim_instances(property_hash[:value]).flatten.reject(&:nil?)
462
+ # Sometimes the instances are an empty array
463
+ unless cim_instance_hashes.count.zero?
464
+ cim_instance_hashes.each do |instance|
465
+ variable_name = random_variable_name
466
+ instantiated_variables.merge!(variable_name => instance)
467
+ class_name = instance['cim_instance_type']
468
+ properties = instance.reject { |k, _v| k == 'cim_instance_type' }
469
+ cim_instances_block << format_ciminstance(variable_name, class_name, properties)
470
+ end
471
+ end
472
+ # We have to handle arrays of CIM instances slightly differently
473
+ if property_hash[:mof_type] =~ /\[\]$/
474
+ class_name = property_hash[:mof_type].gsub('[]', '')
475
+ property_hash[:value].each do |hash|
476
+ variable_name = random_variable_name
477
+ instantiated_variables.merge!(variable_name => hash)
478
+ cim_instances_block << format_ciminstance(variable_name, class_name, hash)
479
+ end
480
+ else
481
+ variable_name = random_variable_name
482
+ instantiated_variables.merge!(variable_name => property_hash[:value])
483
+ class_name = property_hash[:mof_type]
484
+ cim_instances_block << format_ciminstance(variable_name, class_name, property_hash[:value])
485
+ end
486
+ end
487
+ cim_instances_block == [] ? '' : cim_instances_block.join("\n")
488
+ end
489
+
490
+ # Recursively search for and return CIM instances nested in an enumerable
491
+ #
492
+ # @param enumerable [Enumerable] a hash or array which may contain CIM Instances
493
+ # @return [Hash] every discovered hash which does define a CIM Instance
494
+ def nested_cim_instances(enumerable)
495
+ enumerable.collect do |key, value|
496
+ if key.is_a?(Hash) && key.key?('cim_instance_type')
497
+ key
498
+ # TODO: Are there any cim instancees 3 levels deep, or only 2?
499
+ # if so, we should *also* keep searching and processing...
500
+ elsif key.is_a?(Enumerable)
501
+ nested_cim_instances(key)
502
+ elsif value.is_a?(Enumerable)
503
+ nested_cim_instances(value)
504
+ end
505
+ end
506
+ end
507
+
508
+ # Write a line of PowerShell which creates a CIM Instance and assigns it to a variable
509
+ #
510
+ # @param variable_name [String] the name of the Variable to assign the CIM Instance to
511
+ # @param class_name [String] the CIM Class to instantiate
512
+ # @param property_hash [Hash] the Properties which define the CIM Instance
513
+ # @return [String] A line of PowerShell which defines the CIM Instance and stores it to a variable
514
+ def format_ciminstance(variable_name, class_name, property_hash)
515
+ definition = "$#{variable_name} = New-CimInstance -ClientOnly -ClassName '#{class_name}' -Property #{format(property_hash)}"
516
+ # AWFUL HACK to make New-CimInstance happy ; it can't parse an array unless it's an array of Cim Instances
517
+ # definition = definition.gsub("@(@{'cim_instance_type'","[CimInstance[]]@(@{'cim_instance_type'")
518
+ # EVEN WORSE HACK - this one we can't even be sure it's a cim instance...
519
+ # but I don't _think_ anything but nested cim instances show up as hashes inside an array
520
+ definition = definition.gsub('@(@{', '[CimInstance[]]@(@{')
521
+ definition = interpolate_variables(definition)
522
+ definition
523
+ end
524
+
525
+ # Munge a resource definition (as from `should_to_resource`) into valid PowerShell which represents
526
+ # the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
527
+ # defined variables into the hash.
528
+ #
529
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
530
+ # @return [String] A string representing the PowerShell definition of the InvokeParams hash
531
+ def invoke_params(resource)
532
+ params = {
533
+ Name: resource[:dscmeta_resource_friendly_name],
534
+ Method: resource[:dsc_invoke_method],
535
+ Property: {}
536
+ }
537
+ if resource.key?(:dscmeta_module_version)
538
+ params[:ModuleName] = {}
539
+ params[:ModuleName][:ModuleName] = "#{resource[:vendored_modules_path]}/#{resource[:dscmeta_module_name]}/#{resource[:dscmeta_module_name]}.psd1"
540
+ params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
541
+ else
542
+ params[:ModuleName] = resource[:dscmeta_module_name]
543
+ end
544
+ resource[:parameters].each do |property_name, property_hash|
545
+ # strip dsc_ from the beginning of the property name declaration
546
+ name = property_name.to_s.gsub(/^dsc_/, '').to_sym
547
+ params[:Property][name] = if property_hash[:mof_type] == 'PSCredential'
548
+ # format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make
549
+ # the Credential hash interpolable as it will be replaced by a variable reference.
550
+ {
551
+ 'user' => property_hash[:value]['user'],
552
+ 'password' => escape_quotes(property_hash[:value]['password'].unwrap)
553
+ }
554
+ else
555
+ property_hash[:value]
556
+ end
557
+ end
558
+ params_block = interpolate_variables("$InvokeParams = #{format(params)}")
559
+ # HACK: make CIM instances work:
560
+ resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type] =~ /\[\]/ }.each do |_property_name, property_hash|
561
+ formatted_property_hash = interpolate_variables(format(property_hash[:value]))
562
+ params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}")
563
+ end
564
+ params_block
565
+ end
566
+
567
+ # Given a resource definition (as from `should_to_resource`), return a PowerShell script which has
568
+ # all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
569
+ # will correct munge the results for returning to Puppet as a JSON object.
570
+ #
571
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
572
+ # @return [String] A string representing the PowerShell script which will invoke the DSC Resource.
573
+ def ps_script_content(resource)
574
+ template_path = File.expand_path('../', __FILE__)
575
+ # Defines the helper functions
576
+ functions = File.new(template_path + '/invoke_dsc_resource_functions.ps1').read
577
+ # Defines the response hash and the runtime settings
578
+ preamble = File.new(template_path + '/invoke_dsc_resource_preamble.ps1').read
579
+ # The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
580
+ postscript = File.new(template_path + '/invoke_dsc_resource_postscript.ps1').read
581
+ # The blocks define the variables to define for the postscript.
582
+ credential_block = prepare_credentials(resource)
583
+ cim_instances_block = prepare_cim_instances(resource)
584
+ parameters_block = invoke_params(resource)
585
+ # clean them out of the temporary cache now that they're not needed; failure to do so can goof up future executions in this run
586
+ clear_instantiated_variables!
587
+
588
+ content = [functions, preamble, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
589
+ content
590
+ end
591
+
592
+ # Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
593
+ # munging over what is provided in the ruby-pwsh library, as it does not handle unwrapping Sensitive
594
+ # data types or interpolating Credentials.
595
+ #
596
+ # @param value [Object] The object to format into valid PowerShell
597
+ # @return [String] A string representation of the input value as valid PowerShell
598
+ def format(value)
599
+ Pwsh::Util.format_powershell_value(value)
600
+ rescue RuntimeError => e
601
+ raise unless e.message =~ /Sensitive \[value redacted\]/
602
+
603
+ string = Pwsh::Util.format_powershell_value(unwrap(value))
604
+ string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
605
+ end
606
+
607
+ # Unwrap sensitive strings for formatting, even inside an enumerable, appending '#PuppetSensitive'
608
+ # to the end of the string in preparation for gsub cleanup.
609
+ #
610
+ # @param value [Object] The object to unwrap sensitive data inside of
611
+ # @return [Object] The object with any sensitive strings unwrapped and annotated
612
+ def unwrap(value)
613
+ if value.class.name == 'Puppet::Pops::Types::PSensitiveType::Sensitive'
614
+ "#{value.unwrap}#PuppetSensitive"
615
+ elsif value.class.name == 'Hash'
616
+ unwrapped = {}
617
+ value.each do |k, v|
618
+ unwrapped[k] = unwrap(v)
619
+ end
620
+ unwrapped
621
+ elsif value.class.name == 'Array'
622
+ unwrapped = []
623
+ value.each do |v|
624
+ unwrapped << unwrap(v)
625
+ end
626
+ unwrapped
627
+ else
628
+ value
629
+ end
630
+ end
631
+
632
+ # Escape any nested single quotes in a Sensitive string
633
+ #
634
+ # @param text [String] the text to escape
635
+ # @return [String] the escaped text
636
+ def escape_quotes(text)
637
+ text.gsub("'", "''")
638
+ end
639
+
640
+ # While Puppet is aware of Sensitive data types, the PowerShell script is not
641
+ # and so for debugging purposes must be redacted before being sent to debug
642
+ # output but must *not* be redacted when sent to the PowerShell code manager.
643
+ #
644
+ # @param text [String] the text to redact
645
+ # @return [String] the redacted text
646
+ def redact_secrets(text)
647
+ # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
648
+ # no known resources specify a SecureString instead of a PSCredential object. We therefore only
649
+ # need to redact strings which look like password declarations.
650
+ modified_text = text.gsub(/(?<=-Password )'.+' # PuppetSensitive/, "'#<Sensitive [value redacted]>'")
651
+ if modified_text =~ /'.+' # PuppetSensitive/
652
+ # Something has gone wrong, error loudly?
653
+ else
654
+ modified_text
655
+ end
656
+ end
657
+
658
+ # Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
659
+ # Definiing it here allows re-use of a single instance instead of continually instantiating and
660
+ # tearing a new instance down for every call.
661
+ def ps_manager
662
+ debug_output = Puppet::Util::Log.level == :debug
663
+ # TODO: Allow you to specify an alternate path, either to pwsh generally or a specific pwsh path.
664
+ Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
665
+ end
666
+ end
@@ -0,0 +1,120 @@
1
+ function new-pscredential {
2
+ [CmdletBinding()]
3
+ param (
4
+ [parameter(Mandatory = $true,
5
+ ValueFromPipelineByPropertyName = $true)]
6
+ [string]
7
+ $user,
8
+
9
+ [parameter(Mandatory = $true,
10
+ ValueFromPipelineByPropertyName = $true)]
11
+ [string]
12
+ $password
13
+ )
14
+
15
+ $secpasswd = ConvertTo-SecureString $password -AsPlainText -Force
16
+ $credentials = New-Object System.Management.Automation.PSCredential ($user, $secpasswd)
17
+ return $credentials
18
+ }
19
+
20
+ Function ConvertTo-CanonicalResult {
21
+ [CmdletBinding()]
22
+ param(
23
+ [Parameter(Mandatory, Position = 1)]
24
+ [psobject]
25
+ $Result,
26
+
27
+ [Parameter(DontShow)]
28
+ [string]
29
+ $PropertyPath,
30
+
31
+ [Parameter(DontShow)]
32
+ [int]
33
+ $RecursionLevel = 0
34
+ )
35
+
36
+ $MaxDepth = 5
37
+ $CimInstancePropertyFilter = { $_.Definition -match 'CimInstance' -and $_.Name -ne 'PSDscRunAsCredential' }
38
+
39
+ # Get the properties which are/aren't Cim instances
40
+ $ResultObject = @{ }
41
+ $ResultPropertyList = $Result | Get-Member -MemberType Property | Where-Object { $_.Name -ne 'PSComputerName' }
42
+ $CimInstanceProperties = $ResultPropertyList | Where-Object -FilterScript $CimInstancePropertyFilter
43
+
44
+ foreach ($Property in $ResultPropertyList) {
45
+ $PropertyName = $Property.Name
46
+ if ($Property -notin $CimInstanceProperties) {
47
+ $Value = $Result.$PropertyName
48
+ if ($PropertyName -eq 'Ensure' -and [string]::IsNullOrEmpty($Result.$PropertyName)) {
49
+ # Just set 'Present' since it was found /shrug
50
+ # If the value IS listed as absent, don't update it unless you want flapping
51
+ $Value = 'Present'
52
+ }
53
+ else {
54
+ if ($Value -is [string] -or $value -is [string[]]) {
55
+ $Value = $Value
56
+ }
57
+
58
+ if ($Value.Count -eq 1 -and $Property.Definition -match '\\[\\]') {
59
+ $Value = @($Value)
60
+ }
61
+ }
62
+ }
63
+ elseif ($null -eq $Result.$PropertyName) {
64
+ if ($Property -match 'InstanceArray') {
65
+ $Value = @()
66
+ }
67
+ else {
68
+ $Value = $null
69
+ }
70
+ }
71
+ else {
72
+ # Looks like a nested CIM instance, recurse if we're not too deep in already.
73
+ $RecursionLevel++
74
+
75
+ if ($PropertyPath -eq [string]::Empty) {
76
+ $PropertyPath = $PropertyName
77
+ }
78
+ else {
79
+ $PropertyPath = "$PropertyPath.$PropertyName"
80
+ }
81
+
82
+ if ($RecursionLevel -gt $MaxDepth) {
83
+ # Give up recursing more than this
84
+ return $Result.ToString()
85
+ }
86
+
87
+ $Value = foreach ($item in $Result.$PropertyName) {
88
+ ConvertTo-CanonicalResult -Result $item -PropertyPath $PropertyPath -RecursionLevel ($RecursionLevel + 1) -WarningAction Continue
89
+ }
90
+
91
+ # The cim instance type is the last component of the type Name
92
+ # We need to return this for ruby to compare the result hashes
93
+ # We do NOT need it for the top-level properties as those are defined in the type
94
+ If ($RecursionLevel -gt 1 -and ![string]::IsNullOrEmpty($Value) ) {
95
+ # If there's multiple instances, you need to add the type to each one, but you
96
+ # need to specify only *one* name, otherwise things end up *very* broken.
97
+ if ($Value.GetType().Name -match '\[\]') {
98
+ $Value | ForEach-Object -Process {
99
+ $_.cim_instance_type = $Result.$PropertyName.CimClass.CimClassName[0]
100
+ }
101
+ } else {
102
+ $Value.cim_instance_type = $Result.$PropertyName.CimClass.CimClassName
103
+ # Ensure that, if it should be an array, it is
104
+ if ($Result.$PropertyName.GetType().Name -match '\[\]') {
105
+ $Value = @($Value)
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ if ($Property.Definition -match 'InstanceArray') {
112
+ if ($Value.Count -lt 2) { $Value = @($Value) }
113
+ }
114
+
115
+ $ResultObject.$PropertyName = $Value
116
+ }
117
+
118
+ # Output the final result
119
+ $ResultObject
120
+ }
@@ -0,0 +1,23 @@
1
+ Try {
2
+ $Result = Invoke-DscResource @InvokeParams
3
+ } catch {
4
+ $Response.errormessage = $_.Exception.Message
5
+ return ($Response | ConvertTo-Json -Compress)
6
+ }
7
+
8
+ # keep the switch for when Test passes back changed properties
9
+ Switch ($invokeParams.Method) {
10
+ 'Test' {
11
+ $Response.indesiredstate = $Result.InDesiredState
12
+ return ($Response | ConvertTo-Json -Compress)
13
+ }
14
+ 'Set' {
15
+ $Response.indesiredstate = $true
16
+ $Response.rebootrequired = $Result.RebootRequired
17
+ return ($Response | ConvertTo-Json -Compress)
18
+ }
19
+ 'Get' {
20
+ $CanonicalizedResult = ConvertTo-CanonicalResult -Result $Result
21
+ return ($CanonicalizedResult | ConvertTo-Json -Compress -Depth 10)
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ $script:ErrorActionPreference = 'Stop'
2
+ $script:WarningPreference = 'SilentlyContinue'
3
+
4
+ $response = @{
5
+ indesiredstate = $false
6
+ rebootrequired = $false
7
+ errormessage = ''
8
+ }
@@ -471,7 +471,7 @@ Invoke-PowerShellUserCode @params
471
471
  # @return [String] The UTF-8 encoded string containing the payload
472
472
  def self.read_length_prefixed_string!(bytes)
473
473
  # 32 bit integer in Little Endian format
474
- length = bytes.slice!(0, 4).unpack('V').first
474
+ length = bytes.slice!(0, 4).unpack1('V')
475
475
  return nil if length.zero?
476
476
 
477
477
  bytes.slice!(0, length).force_encoding(Encoding::UTF_8)
@@ -586,7 +586,7 @@ Invoke-PowerShellUserCode @params
586
586
 
587
587
  pipe_reader = Thread.new(@pipe) do |pipe|
588
588
  # Read a Little Endian 32-bit integer for length of response
589
- expected_response_length = pipe.sysread(4).unpack('V').first
589
+ expected_response_length = pipe.sysread(4).unpack1('V')
590
590
 
591
591
  next nil if expected_response_length.zero?
592
592
 
@@ -12,7 +12,7 @@ module Pwsh
12
12
  def on_windows?
13
13
  # Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
14
14
  # library uses that to test what platform it's on.
15
- !!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation
15
+ !!File::ALT_SEPARATOR
16
16
  end
17
17
 
18
18
  # Verify paths specified are valid directories which exist.
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Pwsh
4
4
  # The version of the ruby-pwsh gem
5
- VERSION = '0.4.1'
5
+ VERSION = '0.5.0'
6
6
  end
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: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet, Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-02-13 00:00:00.000000000 Z
11
+ date: 2020-08-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: PowerShell code manager for ruby.
14
14
  email:
@@ -31,8 +31,13 @@ files:
31
31
  - LICENSE.txt
32
32
  - README.md
33
33
  - Rakefile
34
+ - appveyor.yml
34
35
  - design-comms.png
35
36
  - lib/puppet/feature/pwshlib.rb
37
+ - lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb
38
+ - lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_functions.ps1
39
+ - lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_postscript.ps1
40
+ - lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_preamble.ps1
36
41
  - lib/pwsh.rb
37
42
  - lib/pwsh/util.rb
38
43
  - lib/pwsh/version.rb