ruby-pwsh 0.2.0 → 0.5.1

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