ruby-pwsh 0.4.1 → 0.6.2

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