ruby-pwsh 0.3.0 → 0.6.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: b9ff0bf2c8d109926cd0161323e4d49e6b697410883d5c6da7e8beda5cf8de6d
4
- data.tar.gz: da85cd63a46c1d75b0b1178d9841f3d3e134ad20dead8847eb724d52bec3a847
3
+ metadata.gz: 164dc89d750a5544dd4c100bd2166c0fe9d26447d5636bb18b0eec7d8dfc142f
4
+ data.tar.gz: bdf317f119bd2e5a7261a711a2a5c4721feda199a955feb5265af07c6f94d8d8
5
5
  SHA512:
6
- metadata.gz: '09007c0add66995ff0a978129c9b3da760d4d5a9a95bad6bf77226afdcbdeab2508ad68ac39c2217bd5bd57be41904b18333dcba14365441c98582291224782a'
7
- data.tar.gz: 2157ae9772fefc54271500a3d7d84502d1737e45fc54986405e45da962fb40fd855d482b4e31aa33febb61756f9d6944f9db82a4fb1f6dcea696ec7f6b81beea
6
+ metadata.gz: 8430d204e301ce7577148a95d37a8581b3e0d37da1c3e97fcb5dce3c3a2b66601b651dd48ef3a2810d802d09cf28579dfc986712bf407e54ecb3ca8f23ab9283
7
+ data.tar.gz: ed31dd3ec848dc4c7fd108ee040c5b39e532ea50fe406ba8f98dfb6bb2cf5c804b0d758c393ead8b560785d34717a629380e346f24baae6963e34094889c6fdc
@@ -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,54 @@
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.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-03)
5
+ ## [0.6.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.6.0) (2020-11-24)
6
+
7
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.5.1...0.6.0)
8
+
9
+ ### Added
10
+
11
+ - \(GH-81\) Handle parameters in the dsc base provider [\#62](https://github.com/puppetlabs/ruby-pwsh/pull/62) ([michaeltlombardi](https://github.com/michaeltlombardi))
12
+ - \(GH-74\) Remove special handling for ensure in the dsc base provider [\#61](https://github.com/puppetlabs/ruby-pwsh/pull/61) ([michaeltlombardi](https://github.com/michaeltlombardi))
13
+ - \(GH-59\) Refactor away from Simple Provider [\#60](https://github.com/puppetlabs/ruby-pwsh/pull/60) ([michaeltlombardi](https://github.com/michaeltlombardi))
14
+
15
+ ### Fixed
16
+
17
+ - \(GH-57\) Handle datetimes in dsc [\#58](https://github.com/puppetlabs/ruby-pwsh/pull/58) ([michaeltlombardi](https://github.com/michaeltlombardi))
18
+ - \(GH-55\) Handle intentionally empty arrays [\#56](https://github.com/puppetlabs/ruby-pwsh/pull/56) ([michaeltlombardi](https://github.com/michaeltlombardi))
19
+
20
+ ## [0.5.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.1) (2020-09-25)
21
+
22
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.5.0...0.5.1)
23
+
24
+ ### Fixed
25
+
26
+ - \(MAINT\) Ensure dsc provider finds dsc resources during agent run [\#45](https://github.com/puppetlabs/ruby-pwsh/pull/45) ([michaeltlombardi](https://github.com/michaeltlombardi))
27
+
28
+ ## [0.5.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.0) (2020-08-20)
29
+
30
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.1...0.5.0)
31
+
32
+ ### Added
33
+
34
+ - \(IAC-1045\) Add the DSC base Puppet provider to pwshlib [\#39](https://github.com/puppetlabs/ruby-pwsh/pull/39) ([michaeltlombardi](https://github.com/michaeltlombardi))
35
+
36
+ ## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-13)
37
+
38
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.0...0.4.1)
39
+
40
+ ### Fixed
41
+
42
+ - Ensure ruby versions older than 2.3 function correctly [\#30](https://github.com/puppetlabs/ruby-pwsh/pull/30) ([binford2k](https://github.com/binford2k))
43
+
44
+ ## [0.4.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.0) (2020-01-14)
45
+
46
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.3.0...0.4.0)
47
+
48
+ ### Added
49
+
50
+ - \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
51
+
52
+ ## [0.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-04)
6
53
 
7
54
  [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.2.0...0.3.0)
8
55
 
@@ -25,4 +72,4 @@ All notable changes to this project will be documented in this file.The format i
25
72
 
26
73
 
27
74
 
28
- \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
75
+ \* *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
data/Rakefile CHANGED
@@ -61,8 +61,6 @@ end
61
61
  desc 'Build the gem'
62
62
  task :build do
63
63
  gemspec_path = File.join(Dir.pwd, 'ruby-pwsh.gemspec')
64
- # Delete the puppet-specific code if it exists
65
- FileUtils.rm_r('lib/puppet') if File.exist?('lib/puppet')
66
64
  run_local_command("bundle exec gem build '#{gemspec_path}'")
67
65
  end
68
66
 
@@ -75,7 +73,7 @@ task :tag, [:version, :sha] do |_task, args|
75
73
  raise "Invalid version #{args[:version]} - must be like '1.2.3'" unless args[:version] =~ /^\d+\.\d+\.\d+$/
76
74
 
77
75
  run_local_command('git fetch upstream')
78
- run_local_command("git tag -a version -m #{args[:version]} #{args[:sha]}")
76
+ run_local_command("git tag -a #{args[:version]} -m #{args[:version]} #{args[:sha]}")
79
77
  run_local_command('git push upstream --tags')
80
78
  end
81
79
 
@@ -0,0 +1,38 @@
1
+ ---
2
+ version: 1.1.x.{build}
3
+ branches:
4
+ only:
5
+ - main
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet/util/feature'
4
+
5
+ Puppet.features.add(:pwshlib, libs: ['ruby-pwsh'])
@@ -0,0 +1,739 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'ruby-pwsh'
5
+ require 'pathname'
6
+ require 'json'
7
+
8
+ class Puppet::Provider::DscBaseProvider
9
+ # Initializes the provider, preparing the class variables which cache:
10
+ # - the canonicalized resources across calls
11
+ # - query results
12
+ # - logon failures
13
+ def initialize
14
+ @@cached_canonicalized_resource = []
15
+ @@cached_query_results = []
16
+ @@logon_failures = []
17
+ super
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
+ # Determines whether a resource is ensurable and which message to write (create, update, or delete),
113
+ # then passes the appropriate values along to the various sub-methods which themselves call the Set
114
+ # method of Invoke-DscResource. Implementation borrowed directly from the Resource API Simple Provider
115
+ #
116
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
117
+ # @param changes [Hash] the hash of whose key is the name_hash and value is the is and should hashes
118
+ def set(context, changes)
119
+ changes.each do |name, change|
120
+ is = change.key?(:is) ? change[:is] : (get(context, [name]) || []).find { |r| r[:name] == name }
121
+ context.type.check_schema(is) unless change.key?(:is)
122
+
123
+ should = change[:should]
124
+
125
+ name_hash = if context.type.namevars.length > 1
126
+ # pass a name_hash containing the values of all namevars
127
+ name_hash = {}
128
+ context.type.namevars.each do |namevar|
129
+ name_hash[namevar] = change[:should][namevar]
130
+ end
131
+ name_hash
132
+ else
133
+ name
134
+ end
135
+
136
+ # for compatibility sake, we use dsc_ensure instead of ensure, so context.type.ensurable? does not work
137
+ if !context.type.attributes.key?(:dsc_ensure)
138
+ is = create_absent(:name, name) if is.nil?
139
+ should = create_absent(:name, name) if should.nil?
140
+
141
+ # HACK: If the DSC Resource is ensurable but doesn't report a default value
142
+ # for ensure, we assume it to be `Present` - this is the most common pattern.
143
+ should_ensure = should[:dsc_ensure].nil? ? 'Present' : should[:dsc_ensure].to_s
144
+ is_ensure = is[:dsc_ensure].to_s
145
+
146
+ if is_ensure == 'Absent' && should_ensure == 'Present'
147
+ context.creating(name) do
148
+ create(context, name_hash, should)
149
+ end
150
+ elsif is_ensure == 'Present' && should_ensure == 'Present'
151
+ context.updating(name) do
152
+ update(context, name_hash, should)
153
+ end
154
+ elsif is_ensure == 'Present' && should_ensure == 'Absent'
155
+ context.deleting(name) do
156
+ delete(context, name_hash)
157
+ end
158
+ end
159
+ else
160
+ context.updating(name) do
161
+ update(context, name_hash, should)
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Creates a hash with the name / name_hash and sets dsc_ensure to absent for comparison
168
+ # purposes; this handles cases where the resource isn't found on the node.
169
+ #
170
+ # @param namevar [Object] the name of the variable being used for the resource name
171
+ # @param title [Hash] the hash of namevar properties and their values
172
+ # @return [Hash] returns a hash representing the absent state of the resource
173
+ def create_absent(namevar, title)
174
+ result = if title.is_a? Hash
175
+ title.dup
176
+ else
177
+ { namevar => title }
178
+ end
179
+ result[:dsc_ensure] = 'Absent'
180
+ result
181
+ end
182
+
183
+ # Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
184
+ # the `invoke_set_method` method; whether this method, `update`, or `delete` is called is entirely
185
+ # up to the Resource API based on the results
186
+ #
187
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
188
+ # @param name [String] the name of the resource being created
189
+ # @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.
190
+ def create(context, name, should)
191
+ context.debug("Creating '#{name}' with #{should.inspect}")
192
+ invoke_set_method(context, name, should)
193
+ end
194
+
195
+ # Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
196
+ # the `invoke_set_method` method; whether this method, `create`, or `delete` is called is entirely
197
+ # up to the Resource API based on the results
198
+ #
199
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
200
+ # @param name [String] the name of the resource being created
201
+ # @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.
202
+ def update(context, name, should)
203
+ context.debug("Updating '#{name}' with #{should.inspect}")
204
+ invoke_set_method(context, name, should)
205
+ end
206
+
207
+ # Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
208
+ # the `invoke_set_method` method; whether this method, `create`, or `update` is called is entirely
209
+ # up to the Resource API based on the results
210
+ #
211
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
212
+ # @param name [String] the name of the resource being created
213
+ # @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.
214
+ def delete(context, name)
215
+ context.debug("Deleting '#{name}'")
216
+ invoke_set_method(context, name, name.merge({ dsc_ensure: 'Absent' }))
217
+ end
218
+
219
+ # Invokes the `Get` method, passing the name_hash as the properties to use with `Invoke-DscResource`
220
+ # The PowerShell script returns a JSON representation of the DSC Resource's CIM Instance munged as
221
+ # best it can be for Ruby. Once that JSON is parsed into a hash this method further munges it to
222
+ # fit the expected property definitions. Finally, it returns the object for the Resource API to
223
+ # compare against and determine what future actions, if any, are needed.
224
+ #
225
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
226
+ # @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
227
+ # @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects
228
+ def invoke_get_method(context, name_hash)
229
+ context.debug("retrieving #{name_hash.inspect}")
230
+
231
+ # Do not bother running if the logon credentials won't work
232
+ return name_hash if !name_hash[:dsc_psdscrunascredential].nil? && logon_failed_already?(name_hash[:dsc_psdscrunascredential])
233
+
234
+ query_props = name_hash.select { |k, v| mandatory_get_attributes(context).include?(k) || (k == :dsc_psdscrunascredential && !v.nil?) }
235
+ resource = should_to_resource(query_props, context, 'get')
236
+ script_content = ps_script_content(resource)
237
+ context.debug("Script:\n #{redact_secrets(script_content)}")
238
+ output = ps_manager.execute(script_content)[:stdout]
239
+ context.err('Nothing returned') if output.nil?
240
+
241
+ data = JSON.parse(output)
242
+ context.debug("raw data received: #{data.inspect}")
243
+ error = data['errormessage']
244
+ unless error.nil?
245
+ # NB: We should have a way to stop processing this resource *now* without blowing up the whole Puppet run
246
+ # Raising an error stops processing but blows things up while context.err alerts but continues to process
247
+ if error =~ /Logon failure: the user has not been granted the requested logon type at this computer/
248
+ logon_error = "PSDscRunAsCredential account specified (#{name_hash[:dsc_psdscrunascredential]['user']}) does not have appropriate logon rights; are they an administrator?"
249
+ name_hash[:name].nil? ? context.err(logon_error) : context.err(name_hash[:name], logon_error)
250
+ @@logon_failures << name_hash[:dsc_psdscrunascredential].dup
251
+ # This is a hack to handle the query cache to prevent a second lookup
252
+ @@cached_query_results << name_hash # if fetch_cached_hashes(@@cached_query_results, [data]).empty?
253
+ else
254
+ context.err(error)
255
+ end
256
+ # Either way, something went wrong and we didn't get back a good result, so return nil
257
+ return nil
258
+ end
259
+ # DSC gives back information we don't care about; filter down to only
260
+ # those properties exposed in the type definition.
261
+ valid_attributes = context.type.attributes.keys.collect(&:to_s)
262
+ parameters = context.type.attributes.select { |_name, properties| [properties[:behaviour]].collect.include?(:parameter) }.keys.collect(&:to_s)
263
+ data.select! { |key, _value| valid_attributes.include?("dsc_#{key.downcase}") }
264
+ data.reject! { |key, _value| parameters.include?("dsc_#{key.downcase}") }
265
+ # Canonicalize the results to match the type definition representation;
266
+ # failure to do so will prevent the resource_api from comparing the result
267
+ # to the should hash retrieved from the resource definition in the manifest.
268
+ data.keys.each do |key| # rubocop:disable Style/HashEachMethods
269
+ type_key = "dsc_#{key.downcase}".to_sym
270
+ data[type_key] = data.delete(key)
271
+ camelcase_hash_keys!(data[type_key]) if data[type_key].is_a?(Enumerable)
272
+ # Convert DateTime back to appropriate type
273
+ data[type_key] = Puppet::Pops::Time::Timestamp.parse(data[type_key]) if context.type.attributes[type_key][:mof_type] =~ /DateTime/i
274
+ # PowerShell does not distinguish between a return of empty array/string
275
+ # and null but Puppet does; revert to those values if specified.
276
+ if data[type_key].nil? && query_props.keys.include?(type_key) && query_props[type_key].is_a?(Array)
277
+ data[type_key] = query_props[type_key].empty? ? query_props[type_key] : []
278
+ end
279
+ end
280
+ # If a resource is found, it's present, so refill this Puppet-only key
281
+ data.merge!({ name: name_hash[:name] })
282
+
283
+ # Cache the query to prevent a second lookup
284
+ @@cached_query_results << data.dup if fetch_cached_hashes(@@cached_query_results, [data]).empty?
285
+ context.debug("Returned to Puppet as #{data}")
286
+ data
287
+ end
288
+
289
+ # Invokes the `Set` method, passing the should hash as the properties to use with `Invoke-DscResource`
290
+ # The PowerShell script returns a JSON hash with key-value pairs indicating whether or not the resource
291
+ # is in the desired state, whether or not it requires a reboot, and any error messages captured.
292
+ #
293
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
294
+ # @param should [Hash] the desired state represented definition to pass as properties to Invoke-DscResource
295
+ # @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.
296
+ def invoke_set_method(context, name, should)
297
+ context.debug("Invoking Set Method for '#{name}' with #{should.inspect}")
298
+
299
+ # Do not bother running if the logon credentials won't work
300
+ return nil if !should[:dsc_psdscrunascredential].nil? && logon_failed_already?(should[:dsc_psdscrunascredential])
301
+
302
+ apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
303
+ resource = should_to_resource(apply_props, context, 'set')
304
+ script_content = ps_script_content(resource)
305
+ context.debug("Script:\n #{redact_secrets(script_content)}")
306
+
307
+ output = ps_manager.execute(script_content)[:stdout]
308
+ context.err('Nothing returned') if output.nil?
309
+
310
+ data = JSON.parse(output)
311
+ context.debug(data)
312
+
313
+ context.err(data['errormessage']) unless data['errormessage'].empty?
314
+ # TODO: Implement this functionality for notifying a DSC reboot?
315
+ # notify_reboot_pending if data['rebootrequired'] == true
316
+ data
317
+ end
318
+
319
+ # Converts a Puppet resource hash into a hash with the information needed to call Invoke-DscResource,
320
+ # including the desired state, the path to the PowerShell module containing the resources, the invoke
321
+ # method, and metadata about the DSC Resource and Puppet Type.
322
+ #
323
+ # @param should [Hash] A hash representing the desired state of the DSC resource as defined in Puppet
324
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
325
+ # @param dsc_invoke_method [String] the method to pass to Invoke-DscResource: get, set, or test
326
+ # @return [Hash] a hash with the information needed to run `Invoke-DscResource`
327
+ def should_to_resource(should, context, dsc_invoke_method)
328
+ resource = {}
329
+ resource[:parameters] = {}
330
+ %i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_module_name dscmeta_module_version].each do |k|
331
+ resource[k] = context.type.definition[k]
332
+ end
333
+ should.each do |k, v|
334
+ # PSDscRunAsCredential is considered a namevar and will always be passed, even if nil
335
+ # To prevent flapping during runs, remove it from the resource definition unless specified
336
+ next if k == :dsc_psdscrunascredential && v.nil?
337
+
338
+ resource[:parameters][k] = {}
339
+ resource[:parameters][k][:value] = v
340
+ %i[mof_type mof_is_embedded].each do |ky|
341
+ resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky]
342
+ end
343
+ end
344
+ resource[:dsc_invoke_method] = dsc_invoke_method
345
+
346
+ # Because Puppet adds all of the modules to the LOAD_PATH we can be sure that the appropriate module lives here during an apply;
347
+ # PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
348
+ # During a Puppet agent run, the code lives in the cache so we can use the file expansion to discover the correct folder.
349
+ root_module_path = $LOAD_PATH.select { |path| path.match?(%r{#{resource[:dscmeta_module_name].downcase}/lib}) }.first
350
+ resource[:vendored_modules_path] = if root_module_path.nil?
351
+ File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources') # rubocop:disable Style/StringConcatenation
352
+ else
353
+ File.expand_path("#{root_module_path}/puppet_x/dsc_resources")
354
+ end
355
+ resource[:attributes] = nil
356
+ context.debug("should_to_resource: #{resource.inspect}")
357
+ resource
358
+ end
359
+
360
+ # Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique
361
+ # variables in the PowerShell script.
362
+ #
363
+ # @return [String] a uuid with underscores instead of dashes.
364
+ def random_variable_name
365
+ # PowerShell variables can't include dashes
366
+ SecureRandom.uuid.gsub('-', '_')
367
+ end
368
+
369
+ # Return a Hash containing all of the variables defined for instantiation as well as the Ruby hash for their
370
+ # properties so they can be matched and replaced as needed.
371
+ #
372
+ # @return [Hash] containing all instantiated variables and the properties that they define
373
+ def instantiated_variables
374
+ @@instantiated_variables ||= {}
375
+ end
376
+
377
+ # Clear the instantiated variables hash to be ready for the next run
378
+ def clear_instantiated_variables!
379
+ @@instantiated_variables = {}
380
+ end
381
+
382
+ # Return true if the specified credential hash has already failed to execute a DSC resource due to
383
+ # a logon error, as when the account is not an administrator on the machine; otherwise returns false.
384
+ #
385
+ # @param [Hash] a credential hash with a user and password keys where the password is a sensitive string
386
+ # @return [Bool] true if the credential_hash has already failed logon, false otherwise
387
+ def logon_failed_already?(credential_hash)
388
+ @@logon_failures.any? do |failure_hash|
389
+ failure_hash['user'] == credential_hash['user'] && failure_hash['password'].unwrap == credential_hash['password'].unwrap
390
+ end
391
+ end
392
+
393
+ # Recursively transforms any enumerable, camelCasing any hash keys it finds
394
+ #
395
+ # @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
396
+ # @return [Enumerable] returns the input object with hash keys recursively camelCased
397
+ def camelcase_hash_keys!(enumerable)
398
+ if enumerable.is_a?(Hash)
399
+ enumerable.keys.each do |key| # rubocop:disable Style/HashEachMethods
400
+ name = key.dup
401
+ name[0] = name[0].downcase
402
+ enumerable[name] = enumerable.delete(key)
403
+ camelcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
404
+ end
405
+ else
406
+ enumerable.each { |item| camelcase_hash_keys!(item) if item.is_a?(Enumerable) }
407
+ end
408
+ end
409
+
410
+ # Recursively transforms any object, downcasing it to enable case insensitive comparisons
411
+ #
412
+ # @param object [Object] a string, array, hash, or other object to attempt to recursively downcase
413
+ # @return [Object] returns the input object recursively downcased
414
+ def recursively_downcase(object)
415
+ case object
416
+ when String
417
+ object.downcase
418
+ when Array
419
+ object.map { |item| recursively_downcase(item) }
420
+ when Hash
421
+ transformed = {}
422
+ object.transform_keys(&:downcase).each do |key, value|
423
+ transformed[key] = recursively_downcase(value)
424
+ end
425
+ transformed
426
+ else
427
+ object
428
+ end
429
+ end
430
+
431
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
432
+ #
433
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
434
+ # @return [Array] returns an array of attribute names as symbols which are mandatory for get operations
435
+ def mandatory_get_attributes(context)
436
+ context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_get] }.keys
437
+ end
438
+
439
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for set operations
440
+ #
441
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
442
+ # @return [Array] returns an array of attribute names as symbols which are mandatory for set operations
443
+ def mandatory_set_attributes(context)
444
+ context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_set] }.keys
445
+ end
446
+
447
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as namevars
448
+ #
449
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
450
+ # @return [Array] returns an array of attribute names as symbols which are namevars
451
+ def namevar_attributes(context)
452
+ context.type.attributes.select { |_attribute, properties| properties[:behaviour] == :namevar }.keys
453
+ end
454
+
455
+ # Look through a fully formatted string, replacing all instances where a value matches the formatted properties
456
+ # of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
457
+ # CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
458
+ #
459
+ # @param string [String] the string of text to search through for places an instantiated variable can be referenced
460
+ # @return [String] the string with references to instantiated variables instead of their properties
461
+ def interpolate_variables(string)
462
+ modified_string = string
463
+ # Always replace later-created variables first as they sometimes were built from earlier ones
464
+ instantiated_variables.reverse_each do |variable_name, ruby_definition|
465
+ modified_string = modified_string.gsub(format(ruby_definition), "$#{variable_name}")
466
+ end
467
+ modified_string
468
+ end
469
+
470
+ # Parses a resource definition (as from `should_to_resource`) for any properties which are PowerShell
471
+ # Credentials. As these values need to be serialized into PSCredential objects, return an array of
472
+ # PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
473
+ # These credential variables can then be simply assigned in the parameter hash where needed.
474
+ #
475
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
476
+ # @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables
477
+ def prepare_credentials(resource)
478
+ credentials_block = []
479
+ resource[:parameters].each do |_property_name, property_hash|
480
+ next unless property_hash[:mof_type] == 'PSCredential'
481
+ next if property_hash[:value].nil?
482
+
483
+ variable_name = random_variable_name
484
+ credential_hash = {
485
+ 'user' => property_hash[:value]['user'],
486
+ 'password' => escape_quotes(property_hash[:value]['password'].unwrap)
487
+ }
488
+ instantiated_variables.merge!(variable_name => credential_hash)
489
+ credentials_block << format_pscredential(variable_name, credential_hash)
490
+ end
491
+ credentials_block.join("\n")
492
+ credentials_block == [] ? '' : credentials_block
493
+ end
494
+
495
+ # Write a line of PowerShell which creates a PSCredential object and assigns it to a variable
496
+ #
497
+ # @param variable_name [String] the name of the Variable to assign the PSCredential object to
498
+ # @param credential_hash [Hash] the Properties which define the PSCredential Object
499
+ # @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
500
+ def format_pscredential(variable_name, credential_hash)
501
+ "$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}' # PuppetSensitive"
502
+ end
503
+
504
+ # Parses a resource definition (as from `should_to_resource`) for any properties which are CIM instances
505
+ # whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
506
+ # those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
507
+ # will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
508
+ # by variable reference.
509
+ #
510
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
511
+ # @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables
512
+ def prepare_cim_instances(resource)
513
+ cim_instances_block = []
514
+ resource[:parameters].each do |_property_name, property_hash|
515
+ next unless property_hash[:mof_is_embedded]
516
+
517
+ # strip dsc_ from the beginning of the property name declaration
518
+ # name = property_name.to_s.gsub(/^dsc_/, '').to_sym
519
+ # Process nested CIM instances first as those neeed to be passed to higher-order
520
+ # instances and must therefore be declared before they must be referenced
521
+ cim_instance_hashes = nested_cim_instances(property_hash[:value]).flatten.reject(&:nil?)
522
+ # Sometimes the instances are an empty array
523
+ unless cim_instance_hashes.count.zero?
524
+ cim_instance_hashes.each do |instance|
525
+ variable_name = random_variable_name
526
+ instantiated_variables.merge!(variable_name => instance)
527
+ class_name = instance['cim_instance_type']
528
+ properties = instance.reject { |k, _v| k == 'cim_instance_type' }
529
+ cim_instances_block << format_ciminstance(variable_name, class_name, properties)
530
+ end
531
+ end
532
+ # We have to handle arrays of CIM instances slightly differently
533
+ if property_hash[:mof_type] =~ /\[\]$/
534
+ class_name = property_hash[:mof_type].gsub('[]', '')
535
+ property_hash[:value].each do |hash|
536
+ variable_name = random_variable_name
537
+ instantiated_variables.merge!(variable_name => hash)
538
+ cim_instances_block << format_ciminstance(variable_name, class_name, hash)
539
+ end
540
+ else
541
+ variable_name = random_variable_name
542
+ instantiated_variables.merge!(variable_name => property_hash[:value])
543
+ class_name = property_hash[:mof_type]
544
+ cim_instances_block << format_ciminstance(variable_name, class_name, property_hash[:value])
545
+ end
546
+ end
547
+ cim_instances_block == [] ? '' : cim_instances_block.join("\n")
548
+ end
549
+
550
+ # Recursively search for and return CIM instances nested in an enumerable
551
+ #
552
+ # @param enumerable [Enumerable] a hash or array which may contain CIM Instances
553
+ # @return [Hash] every discovered hash which does define a CIM Instance
554
+ def nested_cim_instances(enumerable)
555
+ enumerable.collect do |key, value|
556
+ if key.is_a?(Hash) && key.key?('cim_instance_type')
557
+ key
558
+ # TODO: Are there any cim instancees 3 levels deep, or only 2?
559
+ # if so, we should *also* keep searching and processing...
560
+ elsif key.is_a?(Enumerable)
561
+ nested_cim_instances(key)
562
+ elsif value.is_a?(Enumerable)
563
+ nested_cim_instances(value)
564
+ end
565
+ end
566
+ end
567
+
568
+ # Write a line of PowerShell which creates a CIM Instance and assigns it to a variable
569
+ #
570
+ # @param variable_name [String] the name of the Variable to assign the CIM Instance to
571
+ # @param class_name [String] the CIM Class to instantiate
572
+ # @param property_hash [Hash] the Properties which define the CIM Instance
573
+ # @return [String] A line of PowerShell which defines the CIM Instance and stores it to a variable
574
+ def format_ciminstance(variable_name, class_name, property_hash)
575
+ definition = "$#{variable_name} = New-CimInstance -ClientOnly -ClassName '#{class_name}' -Property #{format(property_hash)}"
576
+ # AWFUL HACK to make New-CimInstance happy ; it can't parse an array unless it's an array of Cim Instances
577
+ # definition = definition.gsub("@(@{'cim_instance_type'","[CimInstance[]]@(@{'cim_instance_type'")
578
+ # EVEN WORSE HACK - this one we can't even be sure it's a cim instance...
579
+ # but I don't _think_ anything but nested cim instances show up as hashes inside an array
580
+ definition = definition.gsub('@(@{', '[CimInstance[]]@(@{')
581
+ interpolate_variables(definition)
582
+ end
583
+
584
+ # Munge a resource definition (as from `should_to_resource`) into valid PowerShell which represents
585
+ # the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
586
+ # defined variables into the hash.
587
+ #
588
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
589
+ # @return [String] A string representing the PowerShell definition of the InvokeParams hash
590
+ def invoke_params(resource)
591
+ params = {
592
+ Name: resource[:dscmeta_resource_friendly_name],
593
+ Method: resource[:dsc_invoke_method],
594
+ Property: {}
595
+ }
596
+ if resource.key?(:dscmeta_module_version)
597
+ params[:ModuleName] = {}
598
+ params[:ModuleName][:ModuleName] = "#{resource[:vendored_modules_path]}/#{resource[:dscmeta_module_name]}/#{resource[:dscmeta_module_name]}.psd1"
599
+ params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
600
+ else
601
+ params[:ModuleName] = resource[:dscmeta_module_name]
602
+ end
603
+ resource[:parameters].each do |property_name, property_hash|
604
+ # strip dsc_ from the beginning of the property name declaration
605
+ name = property_name.to_s.gsub(/^dsc_/, '').to_sym
606
+ params[:Property][name] = case property_hash[:mof_type]
607
+ when 'PSCredential'
608
+ # format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make
609
+ # the Credential hash interpolable as it will be replaced by a variable reference.
610
+ {
611
+ 'user' => property_hash[:value]['user'],
612
+ 'password' => escape_quotes(property_hash[:value]['password'].unwrap)
613
+ }
614
+ when 'DateTime'
615
+ # These have to be handled specifically because they rely on the *Puppet* DateTime,
616
+ # not a generic ruby data type (and so can't go in the shared util in ruby-pwsh)
617
+ "[DateTime]#{property_hash[:value].format('%FT%T%z')}"
618
+ else
619
+ property_hash[:value]
620
+ end
621
+ end
622
+ params_block = interpolate_variables("$InvokeParams = #{format(params)}")
623
+ # Move the Apostrophe for DateTime declarations
624
+ params_block = params_block.gsub("'[DateTime]", "[DateTime]'")
625
+ # HACK: Handle intentionally empty arrays - need to strongly type them because
626
+ # CIM instances do not do a consistent job of casting an empty array properly.
627
+ empty_array_parameters = resource[:parameters].select { |_k, v| v[:value].empty? }
628
+ empty_array_parameters.each do |name, properties|
629
+ param_block_name = name.to_s.gsub(/^dsc_/, '')
630
+ params_block = params_block.gsub("#{param_block_name} = @()", "#{param_block_name} = [#{properties[:mof_type]}]@()")
631
+ end
632
+ # HACK: make CIM instances work:
633
+ resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type] =~ /\[\]/ }.each do |_property_name, property_hash|
634
+ formatted_property_hash = interpolate_variables(format(property_hash[:value]))
635
+ params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}")
636
+ end
637
+ params_block
638
+ end
639
+
640
+ # Given a resource definition (as from `should_to_resource`), return a PowerShell script which has
641
+ # all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
642
+ # will correct munge the results for returning to Puppet as a JSON object.
643
+ #
644
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
645
+ # @return [String] A string representing the PowerShell script which will invoke the DSC Resource.
646
+ def ps_script_content(resource)
647
+ template_path = File.expand_path('../', __FILE__)
648
+ # Defines the helper functions
649
+ functions = File.new("#{template_path}/invoke_dsc_resource_functions.ps1").read
650
+ # Defines the response hash and the runtime settings
651
+ preamble = File.new("#{template_path}/invoke_dsc_resource_preamble.ps1").read
652
+ # The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
653
+ postscript = File.new("#{template_path}/invoke_dsc_resource_postscript.ps1").read
654
+ # The blocks define the variables to define for the postscript.
655
+ credential_block = prepare_credentials(resource)
656
+ cim_instances_block = prepare_cim_instances(resource)
657
+ parameters_block = invoke_params(resource)
658
+ # 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
659
+ clear_instantiated_variables!
660
+
661
+ [functions, preamble, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
662
+ end
663
+
664
+ # Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
665
+ # munging over what is provided in the ruby-pwsh library, as it does not handle unwrapping Sensitive
666
+ # data types or interpolating Credentials.
667
+ #
668
+ # @param value [Object] The object to format into valid PowerShell
669
+ # @return [String] A string representation of the input value as valid PowerShell
670
+ def format(value)
671
+ Pwsh::Util.format_powershell_value(value)
672
+ rescue RuntimeError => e
673
+ raise unless e.message =~ /Sensitive \[value redacted\]/
674
+
675
+ string = Pwsh::Util.format_powershell_value(unwrap(value))
676
+ string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
677
+ end
678
+
679
+ # Unwrap sensitive strings for formatting, even inside an enumerable, appending '#PuppetSensitive'
680
+ # to the end of the string in preparation for gsub cleanup.
681
+ #
682
+ # @param value [Object] The object to unwrap sensitive data inside of
683
+ # @return [Object] The object with any sensitive strings unwrapped and annotated
684
+ def unwrap(value)
685
+ case value
686
+ when Puppet::Pops::Types::PSensitiveType::Sensitive
687
+ "#{value.unwrap}#PuppetSensitive"
688
+ when Hash
689
+ unwrapped = {}
690
+ value.each do |k, v|
691
+ unwrapped[k] = unwrap(v)
692
+ end
693
+ unwrapped
694
+ when Array
695
+ unwrapped = []
696
+ value.each do |v|
697
+ unwrapped << unwrap(v)
698
+ end
699
+ unwrapped
700
+ else
701
+ value
702
+ end
703
+ end
704
+
705
+ # Escape any nested single quotes in a Sensitive string
706
+ #
707
+ # @param text [String] the text to escape
708
+ # @return [String] the escaped text
709
+ def escape_quotes(text)
710
+ text.gsub("'", "''")
711
+ end
712
+
713
+ # While Puppet is aware of Sensitive data types, the PowerShell script is not
714
+ # and so for debugging purposes must be redacted before being sent to debug
715
+ # output but must *not* be redacted when sent to the PowerShell code manager.
716
+ #
717
+ # @param text [String] the text to redact
718
+ # @return [String] the redacted text
719
+ def redact_secrets(text)
720
+ # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
721
+ # no known resources specify a SecureString instead of a PSCredential object. We therefore only
722
+ # need to redact strings which look like password declarations.
723
+ modified_text = text.gsub(/(?<=-Password )'.+' # PuppetSensitive/, "'#<Sensitive [value redacted]>'")
724
+ if modified_text =~ /'.+' # PuppetSensitive/
725
+ # Something has gone wrong, error loudly?
726
+ else
727
+ modified_text
728
+ end
729
+ end
730
+
731
+ # Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
732
+ # Definiing it here allows re-use of a single instance instead of continually instantiating and
733
+ # tearing a new instance down for every call.
734
+ def ps_manager
735
+ debug_output = Puppet::Util::Log.level == :debug
736
+ # TODO: Allow you to specify an alternate path, either to pwsh generally or a specific pwsh path.
737
+ Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
738
+ end
739
+ end
@@ -0,0 +1,124 @@
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
+ elseif ($Result.$PropertyName.GetType().Name -match 'DateTime') {
72
+ # Handle DateTimes especially since they're an edge case
73
+ $Value = Get-Date $Result.$PropertyName -UFormat "%Y-%m-%dT%H:%M:%S%Z"
74
+ }
75
+ else {
76
+ # Looks like a nested CIM instance, recurse if we're not too deep in already.
77
+ $RecursionLevel++
78
+
79
+ if ($PropertyPath -eq [string]::Empty) {
80
+ $PropertyPath = $PropertyName
81
+ }
82
+ else {
83
+ $PropertyPath = "$PropertyPath.$PropertyName"
84
+ }
85
+
86
+ if ($RecursionLevel -gt $MaxDepth) {
87
+ # Give up recursing more than this
88
+ return $Result.ToString()
89
+ }
90
+
91
+ $Value = foreach ($item in $Result.$PropertyName) {
92
+ ConvertTo-CanonicalResult -Result $item -PropertyPath $PropertyPath -RecursionLevel ($RecursionLevel + 1) -WarningAction Continue
93
+ }
94
+
95
+ # The cim instance type is the last component of the type Name
96
+ # We need to return this for ruby to compare the result hashes
97
+ # We do NOT need it for the top-level properties as those are defined in the type
98
+ If ($RecursionLevel -gt 1 -and ![string]::IsNullOrEmpty($Value) ) {
99
+ # If there's multiple instances, you need to add the type to each one, but you
100
+ # need to specify only *one* name, otherwise things end up *very* broken.
101
+ if ($Value.GetType().Name -match '\[\]') {
102
+ $Value | ForEach-Object -Process {
103
+ $_.cim_instance_type = $Result.$PropertyName.CimClass.CimClassName[0]
104
+ }
105
+ } else {
106
+ $Value.cim_instance_type = $Result.$PropertyName.CimClass.CimClassName
107
+ # Ensure that, if it should be an array, it is
108
+ if ($Result.$PropertyName.GetType().Name -match '\[\]') {
109
+ $Value = @($Value)
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ if ($Property.Definition -match 'InstanceArray') {
116
+ if ($Value.Count -lt 2) { $Value = @($Value) }
117
+ }
118
+
119
+ $ResultObject.$PropertyName = $Value
120
+ }
121
+
122
+ # Output the final result
123
+ $ResultObject
124
+ }
@@ -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
+ }
@@ -14,10 +14,10 @@ require 'logger'
14
14
  module Pwsh
15
15
  # Standard errors
16
16
  class Error < StandardError; end
17
+
17
18
  # Create an instance of a PowerShell host and manage execution of PowerShell code inside that host.
18
19
  class Manager
19
- attr_reader :powershell_command
20
- attr_reader :powershell_arguments
20
+ attr_reader :powershell_command, :powershell_arguments
21
21
 
22
22
  # We actually want this to be a class variable.
23
23
  @@instances = {} # rubocop:disable Style/ClassVars
@@ -54,7 +54,7 @@ module Pwsh
54
54
  if manager.nil? || !manager.alive?
55
55
  # ignore any errors trying to tear down this unusable instance
56
56
  begin
57
- manager&.exit
57
+ manager.exit unless manager.nil? # rubocop:disable Style/SafeNavigation
58
58
  rescue
59
59
  nil
60
60
  end
@@ -70,7 +70,7 @@ module Pwsh
70
70
  def self.win32console_enabled?
71
71
  @win32console_enabled ||= defined?(Win32) &&
72
72
  defined?(Win32::Console) &&
73
- Win32::Console.class == Class
73
+ Win32::Console.instance_of?(Class)
74
74
  end
75
75
 
76
76
  # TODO: This thing isn't called anywhere and the variable it sets is never referenced...
@@ -117,6 +117,7 @@ module Pwsh
117
117
  # This named pipe path is Windows specific.
118
118
  pipe_path = "\\\\.\\pipe\\#{named_pipe_name}"
119
119
  else
120
+ require 'tmpdir'
120
121
  # .Net implements named pipes under Linux etc. as Unix Sockets in the filesystem
121
122
  # Paths that are rooted are not munged within C# Core.
122
123
  # https://github.com/dotnet/corefx/blob/94e9d02ad70b2224d012ac4a66eaa1f913ae4f29/src/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs#L49-L60
@@ -470,7 +471,7 @@ Invoke-PowerShellUserCode @params
470
471
  # @return [String] The UTF-8 encoded string containing the payload
471
472
  def self.read_length_prefixed_string!(bytes)
472
473
  # 32 bit integer in Little Endian format
473
- length = bytes.slice!(0, 4).unpack('V').first
474
+ length = bytes.slice!(0, 4).unpack1('V')
474
475
  return nil if length.zero?
475
476
 
476
477
  bytes.slice!(0, length).force_encoding(Encoding::UTF_8)
@@ -585,7 +586,7 @@ Invoke-PowerShellUserCode @params
585
586
 
586
587
  pipe_reader = Thread.new(@pipe) do |pipe|
587
588
  # Read a Little Endian 32-bit integer for length of response
588
- expected_response_length = pipe.sysread(4).unpack('V').first
589
+ expected_response_length = pipe.sysread(4).unpack1('V')
589
590
 
590
591
  next nil if expected_response_length.zero?
591
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.
@@ -117,16 +117,16 @@ module Pwsh
117
117
  #
118
118
  # @return [String] representation of the value for interpolation
119
119
  def format_powershell_value(object)
120
- if %i[true false].include?(object) || %w[trueclass falseclass].include?(object.class.name.downcase) # rubocop:disable Lint/BooleanSymbol
120
+ if %i[true false].include?(object) || %w[trueclass falseclass].include?(object.class.name.downcase)
121
121
  "$#{object}"
122
- elsif object.class.name == 'Symbol' || object.class.ancestors.include?(Numeric)
122
+ elsif object.instance_of?(Symbol) || object.class.ancestors.include?(Numeric)
123
123
  object.to_s
124
- elsif object.class.name == 'String'
124
+ elsif object.instance_of?(String)
125
125
  "'#{escape_quotes(object)}'"
126
- elsif object.class.name == 'Array'
127
- '@(' + object.collect { |item| format_powershell_value(item) }.join(', ') + ')'
128
- elsif object.class.name == 'Hash'
129
- '@{' + object.collect { |k, v| format_powershell_value(k) + ' = ' + format_powershell_value(v) }.join('; ') + '}'
126
+ elsif object.instance_of?(Array)
127
+ "@(#{object.collect { |item| format_powershell_value(item) }.join(', ')})"
128
+ elsif object.instance_of?(Hash)
129
+ "@{#{object.collect { |k, v| "#{format_powershell_value(k)} = #{format_powershell_value(v)}" }.join('; ')}}"
130
130
  else
131
131
  raise "unsupported type #{object.class} of value '#{object}'"
132
132
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Pwsh
4
4
  # The version of the ruby-pwsh gem
5
- VERSION = '0.3.0'
5
+ VERSION = '0.6.0'
6
6
  end
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "puppetlabs-pwshlib",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "author": "puppetlabs",
5
5
  "summary": "Provide library code for interoperating with PowerShell.",
6
6
  "license": "MIT",
7
7
  "source": "https://github.com/puppetlabs/ruby-pwsh",
8
- "project_page": "https://github.com/puppetlabs/ruby-pwsh/pwshlib.md",
8
+ "project_page": "https://github.com/puppetlabs/ruby-pwsh/blob/master/pwshlib.md",
9
9
  "issues_url": "https://github.com/puppetlabs/ruby-pwsh/issues",
10
10
  "dependencies": [
11
11
 
@@ -76,7 +76,7 @@
76
76
  "requirements": [
77
77
  {
78
78
  "name": "puppet",
79
- "version_requirement": ">= 4.10.0 < 7.0.0"
79
+ "version_requirement": ">= 5.5.0 < 7.0.0"
80
80
  }
81
81
  ],
82
82
  "pdk-version": "1.13.0",
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.3.0
4
+ version: 0.6.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: 2019-12-04 00:00:00.000000000 Z
11
+ date: 2020-11-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: PowerShell code manager for ruby.
14
14
  email:
@@ -31,7 +31,13 @@ files:
31
31
  - LICENSE.txt
32
32
  - README.md
33
33
  - Rakefile
34
+ - appveyor.yml
34
35
  - design-comms.png
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
35
41
  - lib/pwsh.rb
36
42
  - lib/pwsh/util.rb
37
43
  - lib/pwsh/version.rb