ruby-pwsh 0.3.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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