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