ruby-pwsh 0.4.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f99e2cb873011c1222023d1161d8dd442ee25a41dbbfd46c4f41c34dcae64dec
4
- data.tar.gz: 958293d288ab63a4972a662ef5c9a3441a519558d0513a67dda2c412abb1fbc6
3
+ metadata.gz: 2986a999feb45efb7f137175c7ba987423410fa62ec525f15b877c225d566a05
4
+ data.tar.gz: e966eeca3f8d8eb089016250e7029cdf98290dedd6beff21208e031f5d28dc0b
5
5
  SHA512:
6
- metadata.gz: 9aa8a90d1f9a2647f4b413c7aea243a2d8f1cd59f4885be523c6f2017d4b14eec15e047b8d9b450e148b0d0af8184d4c39025c408bd2abc3054b1919d3125632
7
- data.tar.gz: 5daf8fc2fac2184578b9986e5f4b7aeea4572ff7d1bc84673f7e4ce696741d5d00969331a7356437dd52604dff0aa3a0a97cba62fad3a2fd588068ca62f07601
6
+ metadata.gz: 7f74605239b19db0740f36480507f2a4fe009d7bd566dfa5bfe460e2211c42a7738eb8714d78231e5067163f8257c1d9e45262d1c54ca6d7b37b849681c4108d
7
+ data.tar.gz: ee4a0cfbe10784b763dfb3c4eabbe4b09de537bdf78b2e2e06028afbab9f72b78b3fc4f3960ac3ce979ce2212fd0b5762704b990f8b1a6fb2e230641e4270343
@@ -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,13 +2,61 @@
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.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.0) (2020-01-13)
5
+ ## [0.6.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.6.1) (2020-11-25)
6
+
7
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.6.0...0.6.1)
8
+
9
+ ### Fixed
10
+
11
+ - \(maint\) - Removal of inappropriate terminology [\#70](https://github.com/puppetlabs/ruby-pwsh/pull/70) ([pmcmaw](https://github.com/pmcmaw))
12
+ - \(Maint\) Fix ensurability in the dsc base provider [\#69](https://github.com/puppetlabs/ruby-pwsh/pull/69) ([michaeltlombardi](https://github.com/michaeltlombardi))
13
+
14
+ ## [0.6.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.6.0) (2020-11-24)
15
+
16
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.5.1...0.6.0)
17
+
18
+ ### Added
19
+
20
+ - \(GH-81\) Handle parameters in the dsc base provider [\#62](https://github.com/puppetlabs/ruby-pwsh/pull/62) ([michaeltlombardi](https://github.com/michaeltlombardi))
21
+ - \(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))
22
+ - \(GH-59\) Refactor away from Simple Provider [\#60](https://github.com/puppetlabs/ruby-pwsh/pull/60) ([michaeltlombardi](https://github.com/michaeltlombardi))
23
+
24
+ ### Fixed
25
+
26
+ - \(GH-57\) Handle datetimes in dsc [\#58](https://github.com/puppetlabs/ruby-pwsh/pull/58) ([michaeltlombardi](https://github.com/michaeltlombardi))
27
+ - \(GH-55\) Handle intentionally empty arrays [\#56](https://github.com/puppetlabs/ruby-pwsh/pull/56) ([michaeltlombardi](https://github.com/michaeltlombardi))
28
+
29
+ ## [0.5.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.1) (2020-09-25)
30
+
31
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.5.0...0.5.1)
32
+
33
+ ### Fixed
34
+
35
+ - \(MAINT\) Ensure dsc provider finds dsc resources during agent run [\#45](https://github.com/puppetlabs/ruby-pwsh/pull/45) ([michaeltlombardi](https://github.com/michaeltlombardi))
36
+
37
+ ## [0.5.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.5.0) (2020-08-20)
38
+
39
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.1...0.5.0)
40
+
41
+ ### Added
42
+
43
+ - \(IAC-1045\) Add the DSC base Puppet provider to pwshlib [\#39](https://github.com/puppetlabs/ruby-pwsh/pull/39) ([michaeltlombardi](https://github.com/michaeltlombardi))
44
+
45
+ ## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-13)
46
+
47
+ [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.4.0...0.4.1)
48
+
49
+ ### Fixed
50
+
51
+ - Ensure ruby versions older than 2.3 function correctly [\#30](https://github.com/puppetlabs/ruby-pwsh/pull/30) ([binford2k](https://github.com/binford2k))
52
+
53
+ ## [0.4.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.0) (2020-01-14)
6
54
 
7
55
  [Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.3.0...0.4.0)
8
56
 
9
57
  ### Added
10
58
 
11
- - \(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))
59
+ - \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
12
60
 
13
61
  ## [0.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-04)
14
62
 
@@ -33,4 +81,4 @@ All notable changes to this project will be documented in this file.The format i
33
81
 
34
82
 
35
83
 
36
- \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
84
+ \* *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
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,748 @@
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
+ # Have to check for this to avoid a weird canonicalization warning
284
+ # The Resource API calls canonicalize against the current state which
285
+ # will lead to dsc_ensure being set to absent in the name_hash even if
286
+ # it was set to present in the manifest
287
+ name_hash_has_nil_keys = name_hash.select { |_k, v| v.nil? }.count.positive?
288
+ # We want to throw away all of the empty keys if and only if the manifest
289
+ # declaration is for an absent resource and the resource is actually absent
290
+ data.reject! { |_k, v| v.nil? } if data[:dsc_ensure] == 'Absent' && name_hash[:dsc_ensure] == 'Absent' && !name_hash_has_nil_keys
291
+
292
+ # Cache the query to prevent a second lookup
293
+ @@cached_query_results << data.dup if fetch_cached_hashes(@@cached_query_results, [data]).empty?
294
+ context.debug("Returned to Puppet as #{data}")
295
+ data
296
+ end
297
+
298
+ # Invokes the `Set` method, passing the should hash as the properties to use with `Invoke-DscResource`
299
+ # The PowerShell script returns a JSON hash with key-value pairs indicating whether or not the resource
300
+ # is in the desired state, whether or not it requires a reboot, and any error messages captured.
301
+ #
302
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
303
+ # @param should [Hash] the desired state represented definition to pass as properties to Invoke-DscResource
304
+ # @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.
305
+ def invoke_set_method(context, name, should)
306
+ context.debug("Invoking Set Method for '#{name}' with #{should.inspect}")
307
+
308
+ # Do not bother running if the logon credentials won't work
309
+ return nil if !should[:dsc_psdscrunascredential].nil? && logon_failed_already?(should[:dsc_psdscrunascredential])
310
+
311
+ apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
312
+ resource = should_to_resource(apply_props, context, 'set')
313
+ script_content = ps_script_content(resource)
314
+ context.debug("Script:\n #{redact_secrets(script_content)}")
315
+
316
+ output = ps_manager.execute(script_content)[:stdout]
317
+ context.err('Nothing returned') if output.nil?
318
+
319
+ data = JSON.parse(output)
320
+ context.debug(data)
321
+
322
+ context.err(data['errormessage']) unless data['errormessage'].empty?
323
+ # TODO: Implement this functionality for notifying a DSC reboot?
324
+ # notify_reboot_pending if data['rebootrequired'] == true
325
+ data
326
+ end
327
+
328
+ # Converts a Puppet resource hash into a hash with the information needed to call Invoke-DscResource,
329
+ # including the desired state, the path to the PowerShell module containing the resources, the invoke
330
+ # method, and metadata about the DSC Resource and Puppet Type.
331
+ #
332
+ # @param should [Hash] A hash representing the desired state of the DSC resource as defined in Puppet
333
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
334
+ # @param dsc_invoke_method [String] the method to pass to Invoke-DscResource: get, set, or test
335
+ # @return [Hash] a hash with the information needed to run `Invoke-DscResource`
336
+ def should_to_resource(should, context, dsc_invoke_method)
337
+ resource = {}
338
+ resource[:parameters] = {}
339
+ %i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_module_name dscmeta_module_version].each do |k|
340
+ resource[k] = context.type.definition[k]
341
+ end
342
+ should.each do |k, v|
343
+ # PSDscRunAsCredential is considered a namevar and will always be passed, even if nil
344
+ # To prevent flapping during runs, remove it from the resource definition unless specified
345
+ next if k == :dsc_psdscrunascredential && v.nil?
346
+
347
+ resource[:parameters][k] = {}
348
+ resource[:parameters][k][:value] = v
349
+ %i[mof_type mof_is_embedded].each do |ky|
350
+ resource[:parameters][k][ky] = context.type.definition[:attributes][k][ky]
351
+ end
352
+ end
353
+ resource[:dsc_invoke_method] = dsc_invoke_method
354
+
355
+ # Because Puppet adds all of the modules to the LOAD_PATH we can be sure that the appropriate module lives here during an apply;
356
+ # PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
357
+ # During a Puppet agent run, the code lives in the cache so we can use the file expansion to discover the correct folder.
358
+ root_module_path = $LOAD_PATH.select { |path| path.match?(%r{#{resource[:dscmeta_module_name].downcase}/lib}) }.first
359
+ resource[:vendored_modules_path] = if root_module_path.nil?
360
+ File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources') # rubocop:disable Style/StringConcatenation
361
+ else
362
+ File.expand_path("#{root_module_path}/puppet_x/dsc_resources")
363
+ end
364
+ resource[:attributes] = nil
365
+ context.debug("should_to_resource: #{resource.inspect}")
366
+ resource
367
+ end
368
+
369
+ # Return a UUID with the dashes turned into underscores to enable the specifying of guaranteed-unique
370
+ # variables in the PowerShell script.
371
+ #
372
+ # @return [String] a uuid with underscores instead of dashes.
373
+ def random_variable_name
374
+ # PowerShell variables can't include dashes
375
+ SecureRandom.uuid.gsub('-', '_')
376
+ end
377
+
378
+ # Return a Hash containing all of the variables defined for instantiation as well as the Ruby hash for their
379
+ # properties so they can be matched and replaced as needed.
380
+ #
381
+ # @return [Hash] containing all instantiated variables and the properties that they define
382
+ def instantiated_variables
383
+ @@instantiated_variables ||= {}
384
+ end
385
+
386
+ # Clear the instantiated variables hash to be ready for the next run
387
+ def clear_instantiated_variables!
388
+ @@instantiated_variables = {}
389
+ end
390
+
391
+ # Return true if the specified credential hash has already failed to execute a DSC resource due to
392
+ # a logon error, as when the account is not an administrator on the machine; otherwise returns false.
393
+ #
394
+ # @param [Hash] a credential hash with a user and password keys where the password is a sensitive string
395
+ # @return [Bool] true if the credential_hash has already failed logon, false otherwise
396
+ def logon_failed_already?(credential_hash)
397
+ @@logon_failures.any? do |failure_hash|
398
+ failure_hash['user'] == credential_hash['user'] && failure_hash['password'].unwrap == credential_hash['password'].unwrap
399
+ end
400
+ end
401
+
402
+ # Recursively transforms any enumerable, camelCasing any hash keys it finds
403
+ #
404
+ # @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
405
+ # @return [Enumerable] returns the input object with hash keys recursively camelCased
406
+ def camelcase_hash_keys!(enumerable)
407
+ if enumerable.is_a?(Hash)
408
+ enumerable.keys.each do |key| # rubocop:disable Style/HashEachMethods
409
+ name = key.dup
410
+ name[0] = name[0].downcase
411
+ enumerable[name] = enumerable.delete(key)
412
+ camelcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
413
+ end
414
+ else
415
+ enumerable.each { |item| camelcase_hash_keys!(item) if item.is_a?(Enumerable) }
416
+ end
417
+ end
418
+
419
+ # Recursively transforms any object, downcasing it to enable case insensitive comparisons
420
+ #
421
+ # @param object [Object] a string, array, hash, or other object to attempt to recursively downcase
422
+ # @return [Object] returns the input object recursively downcased
423
+ def recursively_downcase(object)
424
+ case object
425
+ when String
426
+ object.downcase
427
+ when Array
428
+ object.map { |item| recursively_downcase(item) }
429
+ when Hash
430
+ transformed = {}
431
+ object.transform_keys(&:downcase).each do |key, value|
432
+ transformed[key] = recursively_downcase(value)
433
+ end
434
+ transformed
435
+ else
436
+ object
437
+ end
438
+ end
439
+
440
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
441
+ #
442
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
443
+ # @return [Array] returns an array of attribute names as symbols which are mandatory for get operations
444
+ def mandatory_get_attributes(context)
445
+ context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_get] }.keys
446
+ end
447
+
448
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for set operations
449
+ #
450
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
451
+ # @return [Array] returns an array of attribute names as symbols which are mandatory for set operations
452
+ def mandatory_set_attributes(context)
453
+ context.type.attributes.select { |_attribute, properties| properties[:mandatory_for_set] }.keys
454
+ end
455
+
456
+ # Parses the DSC resource type definition to retrieve the names of any attributes which are specified as namevars
457
+ #
458
+ # @param context [Object] the Puppet runtime context to operate in and send feedback to
459
+ # @return [Array] returns an array of attribute names as symbols which are namevars
460
+ def namevar_attributes(context)
461
+ context.type.attributes.select { |_attribute, properties| properties[:behaviour] == :namevar }.keys
462
+ end
463
+
464
+ # Look through a fully formatted string, replacing all instances where a value matches the formatted properties
465
+ # of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
466
+ # CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
467
+ #
468
+ # @param string [String] the string of text to search through for places an instantiated variable can be referenced
469
+ # @return [String] the string with references to instantiated variables instead of their properties
470
+ def interpolate_variables(string)
471
+ modified_string = string
472
+ # Always replace later-created variables first as they sometimes were built from earlier ones
473
+ instantiated_variables.reverse_each do |variable_name, ruby_definition|
474
+ modified_string = modified_string.gsub(format(ruby_definition), "$#{variable_name}")
475
+ end
476
+ modified_string
477
+ end
478
+
479
+ # Parses a resource definition (as from `should_to_resource`) for any properties which are PowerShell
480
+ # Credentials. As these values need to be serialized into PSCredential objects, return an array of
481
+ # PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
482
+ # These credential variables can then be simply assigned in the parameter hash where needed.
483
+ #
484
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
485
+ # @return [String] An array of lines of PowerShell to instantiate PSCredentialObjects and store them in variables
486
+ def prepare_credentials(resource)
487
+ credentials_block = []
488
+ resource[:parameters].each do |_property_name, property_hash|
489
+ next unless property_hash[:mof_type] == 'PSCredential'
490
+ next if property_hash[:value].nil?
491
+
492
+ variable_name = random_variable_name
493
+ credential_hash = {
494
+ 'user' => property_hash[:value]['user'],
495
+ 'password' => escape_quotes(property_hash[:value]['password'].unwrap)
496
+ }
497
+ instantiated_variables.merge!(variable_name => credential_hash)
498
+ credentials_block << format_pscredential(variable_name, credential_hash)
499
+ end
500
+ credentials_block.join("\n")
501
+ credentials_block == [] ? '' : credentials_block
502
+ end
503
+
504
+ # Write a line of PowerShell which creates a PSCredential object and assigns it to a variable
505
+ #
506
+ # @param variable_name [String] the name of the Variable to assign the PSCredential object to
507
+ # @param credential_hash [Hash] the Properties which define the PSCredential Object
508
+ # @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
509
+ def format_pscredential(variable_name, credential_hash)
510
+ "$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}' # PuppetSensitive"
511
+ end
512
+
513
+ # Parses a resource definition (as from `should_to_resource`) for any properties which are CIM instances
514
+ # whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
515
+ # those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
516
+ # will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
517
+ # by variable reference.
518
+ #
519
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
520
+ # @return [String] An array of lines of PowerShell to instantiate CIM Instances and store them in variables
521
+ def prepare_cim_instances(resource)
522
+ cim_instances_block = []
523
+ resource[:parameters].each do |_property_name, property_hash|
524
+ next unless property_hash[:mof_is_embedded]
525
+
526
+ # strip dsc_ from the beginning of the property name declaration
527
+ # name = property_name.to_s.gsub(/^dsc_/, '').to_sym
528
+ # Process nested CIM instances first as those neeed to be passed to higher-order
529
+ # instances and must therefore be declared before they must be referenced
530
+ cim_instance_hashes = nested_cim_instances(property_hash[:value]).flatten.reject(&:nil?)
531
+ # Sometimes the instances are an empty array
532
+ unless cim_instance_hashes.count.zero?
533
+ cim_instance_hashes.each do |instance|
534
+ variable_name = random_variable_name
535
+ instantiated_variables.merge!(variable_name => instance)
536
+ class_name = instance['cim_instance_type']
537
+ properties = instance.reject { |k, _v| k == 'cim_instance_type' }
538
+ cim_instances_block << format_ciminstance(variable_name, class_name, properties)
539
+ end
540
+ end
541
+ # We have to handle arrays of CIM instances slightly differently
542
+ if property_hash[:mof_type] =~ /\[\]$/
543
+ class_name = property_hash[:mof_type].gsub('[]', '')
544
+ property_hash[:value].each do |hash|
545
+ variable_name = random_variable_name
546
+ instantiated_variables.merge!(variable_name => hash)
547
+ cim_instances_block << format_ciminstance(variable_name, class_name, hash)
548
+ end
549
+ else
550
+ variable_name = random_variable_name
551
+ instantiated_variables.merge!(variable_name => property_hash[:value])
552
+ class_name = property_hash[:mof_type]
553
+ cim_instances_block << format_ciminstance(variable_name, class_name, property_hash[:value])
554
+ end
555
+ end
556
+ cim_instances_block == [] ? '' : cim_instances_block.join("\n")
557
+ end
558
+
559
+ # Recursively search for and return CIM instances nested in an enumerable
560
+ #
561
+ # @param enumerable [Enumerable] a hash or array which may contain CIM Instances
562
+ # @return [Hash] every discovered hash which does define a CIM Instance
563
+ def nested_cim_instances(enumerable)
564
+ enumerable.collect do |key, value|
565
+ if key.is_a?(Hash) && key.key?('cim_instance_type')
566
+ key
567
+ # TODO: Are there any cim instancees 3 levels deep, or only 2?
568
+ # if so, we should *also* keep searching and processing...
569
+ elsif key.is_a?(Enumerable)
570
+ nested_cim_instances(key)
571
+ elsif value.is_a?(Enumerable)
572
+ nested_cim_instances(value)
573
+ end
574
+ end
575
+ end
576
+
577
+ # Write a line of PowerShell which creates a CIM Instance and assigns it to a variable
578
+ #
579
+ # @param variable_name [String] the name of the Variable to assign the CIM Instance to
580
+ # @param class_name [String] the CIM Class to instantiate
581
+ # @param property_hash [Hash] the Properties which define the CIM Instance
582
+ # @return [String] A line of PowerShell which defines the CIM Instance and stores it to a variable
583
+ def format_ciminstance(variable_name, class_name, property_hash)
584
+ definition = "$#{variable_name} = New-CimInstance -ClientOnly -ClassName '#{class_name}' -Property #{format(property_hash)}"
585
+ # AWFUL HACK to make New-CimInstance happy ; it can't parse an array unless it's an array of Cim Instances
586
+ # definition = definition.gsub("@(@{'cim_instance_type'","[CimInstance[]]@(@{'cim_instance_type'")
587
+ # EVEN WORSE HACK - this one we can't even be sure it's a cim instance...
588
+ # but I don't _think_ anything but nested cim instances show up as hashes inside an array
589
+ definition = definition.gsub('@(@{', '[CimInstance[]]@(@{')
590
+ interpolate_variables(definition)
591
+ end
592
+
593
+ # Munge a resource definition (as from `should_to_resource`) into valid PowerShell which represents
594
+ # the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
595
+ # defined variables into the hash.
596
+ #
597
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
598
+ # @return [String] A string representing the PowerShell definition of the InvokeParams hash
599
+ def invoke_params(resource)
600
+ params = {
601
+ Name: resource[:dscmeta_resource_friendly_name],
602
+ Method: resource[:dsc_invoke_method],
603
+ Property: {}
604
+ }
605
+ if resource.key?(:dscmeta_module_version)
606
+ params[:ModuleName] = {}
607
+ params[:ModuleName][:ModuleName] = "#{resource[:vendored_modules_path]}/#{resource[:dscmeta_module_name]}/#{resource[:dscmeta_module_name]}.psd1"
608
+ params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
609
+ else
610
+ params[:ModuleName] = resource[:dscmeta_module_name]
611
+ end
612
+ resource[:parameters].each do |property_name, property_hash|
613
+ # strip dsc_ from the beginning of the property name declaration
614
+ name = property_name.to_s.gsub(/^dsc_/, '').to_sym
615
+ params[:Property][name] = case property_hash[:mof_type]
616
+ when 'PSCredential'
617
+ # format can't unwrap Sensitive strings nested in arbitrary hashes/etc, so make
618
+ # the Credential hash interpolable as it will be replaced by a variable reference.
619
+ {
620
+ 'user' => property_hash[:value]['user'],
621
+ 'password' => escape_quotes(property_hash[:value]['password'].unwrap)
622
+ }
623
+ when 'DateTime'
624
+ # These have to be handled specifically because they rely on the *Puppet* DateTime,
625
+ # not a generic ruby data type (and so can't go in the shared util in ruby-pwsh)
626
+ "[DateTime]#{property_hash[:value].format('%FT%T%z')}"
627
+ else
628
+ property_hash[:value]
629
+ end
630
+ end
631
+ params_block = interpolate_variables("$InvokeParams = #{format(params)}")
632
+ # Move the Apostrophe for DateTime declarations
633
+ params_block = params_block.gsub("'[DateTime]", "[DateTime]'")
634
+ # HACK: Handle intentionally empty arrays - need to strongly type them because
635
+ # CIM instances do not do a consistent job of casting an empty array properly.
636
+ empty_array_parameters = resource[:parameters].select { |_k, v| v[:value].empty? }
637
+ empty_array_parameters.each do |name, properties|
638
+ param_block_name = name.to_s.gsub(/^dsc_/, '')
639
+ params_block = params_block.gsub("#{param_block_name} = @()", "#{param_block_name} = [#{properties[:mof_type]}]@()")
640
+ end
641
+ # HACK: make CIM instances work:
642
+ resource[:parameters].select { |_key, hash| hash[:mof_is_embedded] && hash[:mof_type] =~ /\[\]/ }.each do |_property_name, property_hash|
643
+ formatted_property_hash = interpolate_variables(format(property_hash[:value]))
644
+ params_block = params_block.gsub(formatted_property_hash, "[CimInstance[]]#{formatted_property_hash}")
645
+ end
646
+ params_block
647
+ end
648
+
649
+ # Given a resource definition (as from `should_to_resource`), return a PowerShell script which has
650
+ # all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
651
+ # will correct munge the results for returning to Puppet as a JSON object.
652
+ #
653
+ # @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
654
+ # @return [String] A string representing the PowerShell script which will invoke the DSC Resource.
655
+ def ps_script_content(resource)
656
+ template_path = File.expand_path('../', __FILE__)
657
+ # Defines the helper functions
658
+ functions = File.new("#{template_path}/invoke_dsc_resource_functions.ps1").read
659
+ # Defines the response hash and the runtime settings
660
+ preamble = File.new("#{template_path}/invoke_dsc_resource_preamble.ps1").read
661
+ # The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
662
+ postscript = File.new("#{template_path}/invoke_dsc_resource_postscript.ps1").read
663
+ # The blocks define the variables to define for the postscript.
664
+ credential_block = prepare_credentials(resource)
665
+ cim_instances_block = prepare_cim_instances(resource)
666
+ parameters_block = invoke_params(resource)
667
+ # 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
668
+ clear_instantiated_variables!
669
+
670
+ [functions, preamble, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
671
+ end
672
+
673
+ # Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
674
+ # munging over what is provided in the ruby-pwsh library, as it does not handle unwrapping Sensitive
675
+ # data types or interpolating Credentials.
676
+ #
677
+ # @param value [Object] The object to format into valid PowerShell
678
+ # @return [String] A string representation of the input value as valid PowerShell
679
+ def format(value)
680
+ Pwsh::Util.format_powershell_value(value)
681
+ rescue RuntimeError => e
682
+ raise unless e.message =~ /Sensitive \[value redacted\]/
683
+
684
+ string = Pwsh::Util.format_powershell_value(unwrap(value))
685
+ string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
686
+ end
687
+
688
+ # Unwrap sensitive strings for formatting, even inside an enumerable, appending '#PuppetSensitive'
689
+ # to the end of the string in preparation for gsub cleanup.
690
+ #
691
+ # @param value [Object] The object to unwrap sensitive data inside of
692
+ # @return [Object] The object with any sensitive strings unwrapped and annotated
693
+ def unwrap(value)
694
+ case value
695
+ when Puppet::Pops::Types::PSensitiveType::Sensitive
696
+ "#{value.unwrap}#PuppetSensitive"
697
+ when Hash
698
+ unwrapped = {}
699
+ value.each do |k, v|
700
+ unwrapped[k] = unwrap(v)
701
+ end
702
+ unwrapped
703
+ when Array
704
+ unwrapped = []
705
+ value.each do |v|
706
+ unwrapped << unwrap(v)
707
+ end
708
+ unwrapped
709
+ else
710
+ value
711
+ end
712
+ end
713
+
714
+ # Escape any nested single quotes in a Sensitive string
715
+ #
716
+ # @param text [String] the text to escape
717
+ # @return [String] the escaped text
718
+ def escape_quotes(text)
719
+ text.gsub("'", "''")
720
+ end
721
+
722
+ # While Puppet is aware of Sensitive data types, the PowerShell script is not
723
+ # and so for debugging purposes must be redacted before being sent to debug
724
+ # output but must *not* be redacted when sent to the PowerShell code manager.
725
+ #
726
+ # @param text [String] the text to redact
727
+ # @return [String] the redacted text
728
+ def redact_secrets(text)
729
+ # Every secret unwrapped in this module will unwrap as "'secret' # PuppetSensitive" and, currently,
730
+ # no known resources specify a SecureString instead of a PSCredential object. We therefore only
731
+ # need to redact strings which look like password declarations.
732
+ modified_text = text.gsub(/(?<=-Password )'.+' # PuppetSensitive/, "'#<Sensitive [value redacted]>'")
733
+ if modified_text =~ /'.+' # PuppetSensitive/
734
+ # Something has gone wrong, error loudly?
735
+ else
736
+ modified_text
737
+ end
738
+ end
739
+
740
+ # Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
741
+ # Definiing it here allows re-use of a single instance instead of continually instantiating and
742
+ # tearing a new instance down for every call.
743
+ def ps_manager
744
+ debug_output = Puppet::Util::Log.level == :debug
745
+ # TODO: Allow you to specify an alternate path, either to pwsh generally or a specific pwsh path.
746
+ Pwsh::Manager.instance(Pwsh::Manager.powershell_path, Pwsh::Manager.powershell_args, debug: debug_output)
747
+ end
748
+ end