ruby-pwsh 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +109 -0
- data/.gitignore +4 -1
- data/CHANGELOG.md +24 -2
- data/Gemfile +5 -1
- data/README.md +12 -0
- data/Rakefile +30 -0
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +305 -137
- data/lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_functions.ps1 +5 -3
- data/lib/pwsh.rb +19 -45
- data/lib/pwsh/version.rb +1 -1
- data/lib/templates/RubyPwsh.cs +302 -0
- data/lib/templates/init.ps1 +137 -447
- data/metadata.json +30 -33
- metadata +5 -5
- data/.travis.yml +0 -26
- data/appveyor.yml +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4290cf1b35c8aa2ca227d4db7dd4adc437f7e40b060ea88a233cf3fe33ddff98
|
4
|
+
data.tar.gz: ebc560396873912932449a51a694b4d0e03b007f8311cc385bb4e03f368f0d4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6f766d3740eda4d7046f0c0c20bca5c5569433a05c463fd8bee31da759a20ad050ff0c5e7701b46449bc9a50b2f0c3258769f4bec89e7496809d9be001fe2f7
|
7
|
+
data.tar.gz: 1a3fbe17709118963af1a278868910db76996185bca5b76200df6d3864cfb6c41a794a4f741163cd29542a70c5a371d436d2331f52e670bfa53277f89a830a83
|
@@ -0,0 +1,109 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
schedule:
|
6
|
+
- cron: "0 0 * * *"
|
7
|
+
workflow_dispatch:
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
rubocop:
|
11
|
+
runs-on: ${{ matrix.os }}
|
12
|
+
strategy:
|
13
|
+
fail-fast: false
|
14
|
+
matrix:
|
15
|
+
os:
|
16
|
+
- windows-latest
|
17
|
+
- ubuntu-latest
|
18
|
+
ruby: ["2.7"]
|
19
|
+
steps:
|
20
|
+
- name: Checkout Source
|
21
|
+
uses: actions/checkout@v2
|
22
|
+
- name: Activate Ruby
|
23
|
+
uses: ruby/setup-ruby@v1
|
24
|
+
with:
|
25
|
+
ruby-version: ${{ matrix.ruby }}
|
26
|
+
bundler-cache: true
|
27
|
+
- name: Print Test Environment
|
28
|
+
run: |
|
29
|
+
ruby -v
|
30
|
+
gem -v
|
31
|
+
bundle -v
|
32
|
+
pwsh -v
|
33
|
+
- name: Run Rubocop Tests
|
34
|
+
run: |
|
35
|
+
bundle exec rake rubocop
|
36
|
+
spec:
|
37
|
+
runs-on: ${{ matrix.os }}
|
38
|
+
strategy:
|
39
|
+
fail-fast: false
|
40
|
+
matrix:
|
41
|
+
os:
|
42
|
+
- windows-latest
|
43
|
+
- windows-2016
|
44
|
+
- ubuntu-latest
|
45
|
+
- ubuntu-18.04
|
46
|
+
ruby: ["2.5", "2.7"]
|
47
|
+
steps:
|
48
|
+
- name: Checkout Source
|
49
|
+
uses: actions/checkout@v2
|
50
|
+
- name: Activate Ruby
|
51
|
+
uses: ruby/setup-ruby@v1
|
52
|
+
with:
|
53
|
+
ruby-version: ${{ matrix.ruby }}
|
54
|
+
bundler-cache: true
|
55
|
+
- name: Print Test Environment
|
56
|
+
run: |
|
57
|
+
ruby -v
|
58
|
+
gem -v
|
59
|
+
bundle -v
|
60
|
+
pwsh -v
|
61
|
+
- name: Run Spec Tests
|
62
|
+
run: |
|
63
|
+
bundle exec rake spec
|
64
|
+
acceptance-dsc:
|
65
|
+
runs-on: ${{ matrix.os }}
|
66
|
+
strategy:
|
67
|
+
fail-fast: false
|
68
|
+
matrix:
|
69
|
+
os:
|
70
|
+
- windows-latest
|
71
|
+
- windows-2016
|
72
|
+
puppet:
|
73
|
+
- 6
|
74
|
+
- 7
|
75
|
+
include:
|
76
|
+
- puppet: 6
|
77
|
+
ruby: 2.5
|
78
|
+
- puppet: 7
|
79
|
+
ruby: 2.7
|
80
|
+
env:
|
81
|
+
PUPPET_GEM_VERSION: ${{ matrix.puppet }}
|
82
|
+
steps:
|
83
|
+
- name: Checkout Source
|
84
|
+
uses: actions/checkout@v2
|
85
|
+
- name: Activate Ruby
|
86
|
+
uses: ruby/setup-ruby@v1
|
87
|
+
with:
|
88
|
+
ruby-version: ${{ matrix.ruby }}
|
89
|
+
bundler-cache: true
|
90
|
+
- name: Print Test Environment
|
91
|
+
run: |
|
92
|
+
ruby -v
|
93
|
+
gem -v
|
94
|
+
bundle -v
|
95
|
+
pwsh -v
|
96
|
+
- name: Ensure WinRM is working
|
97
|
+
shell: powershell
|
98
|
+
run: |
|
99
|
+
Get-ChildItem WSMan:\localhost\Listener\ -OutVariable Listeners | Format-List * -Force
|
100
|
+
$HTTPListener = $Listeners | Where-Object -FilterScript { $_.Keys.Contains('Transport=HTTP') }
|
101
|
+
If ($HTTPListener.Count -eq 0) {
|
102
|
+
winrm create winrm/config/Listener?Address=*+Transport=HTTP
|
103
|
+
winrm e winrm/config/listener
|
104
|
+
}
|
105
|
+
- name: Run Acceptance Tests
|
106
|
+
shell: powershell
|
107
|
+
run: |
|
108
|
+
bundle exec rake dsc:acceptance:spec_prep
|
109
|
+
bundle exec rake dsc:acceptance:spec
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,14 +2,36 @@
|
|
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.
|
5
|
+
## [0.9.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.9.0) (2021-06-28)
|
6
6
|
|
7
|
-
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.
|
7
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.8.0...0.9.0)
|
8
8
|
|
9
9
|
### Added
|
10
10
|
|
11
|
+
- \(GH-147\) Refactor Invocation methods to use shared helper and write error logs when appropriate [\#152](https://github.com/puppetlabs/ruby-pwsh/pull/152) ([david22swan](https://github.com/david22swan))
|
12
|
+
- \(GH-145\) Improve DSC secrets redaction [\#150](https://github.com/puppetlabs/ruby-pwsh/pull/150) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
13
|
+
- \(GH-145\) Add insync? and invoke\_test\_method to dsc provider [\#124](https://github.com/puppetlabs/ruby-pwsh/pull/124) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
11
14
|
- \(MAINT\) Clarify supported platforms [\#113](https://github.com/puppetlabs/ruby-pwsh/pull/113) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
12
15
|
|
16
|
+
### Fixed
|
17
|
+
|
18
|
+
- \(IAC-1657\) Fix for invalid DateTime value error in `invoke_get_method` [\#169](https://github.com/puppetlabs/ruby-pwsh/pull/169) ([david22swan](https://github.com/david22swan))
|
19
|
+
- \(GH-154\) Ensure values returned from `invoke_get_method` are recursively sorted in the DSC Base Provider to reduce canonicalization warnings. [\#160](https://github.com/puppetlabs/ruby-pwsh/pull/160) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
20
|
+
- \(GH-154\) Fix return data from `Invoke-DscResource` for empty strings and single item arrays in DSC Base Provider [\#159](https://github.com/puppetlabs/ruby-pwsh/pull/159) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
21
|
+
- \(GH-155\) Fix CIM Instance munging in `invoke_get_method` for DSC Base Provider [\#158](https://github.com/puppetlabs/ruby-pwsh/pull/158) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
22
|
+
- \(GH-154\) Fix canonicalization in `get` method for DSC Base Provider [\#157](https://github.com/puppetlabs/ruby-pwsh/pull/157) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
23
|
+
- \(GH-144\) Enable order-insensitive comparisons for DSC [\#151](https://github.com/puppetlabs/ruby-pwsh/pull/151) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
24
|
+
- \(GH-143\) Handle order insensitive arrays in the `same?` method of the DSC Base Provider [\#148](https://github.com/puppetlabs/ruby-pwsh/pull/148) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
25
|
+
- \(GH-127\) Canonicalize enums correctly [\#131](https://github.com/puppetlabs/ruby-pwsh/pull/131) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
26
|
+
- \(GH-125\) Fix dsc provider canonicalization for absent resources [\#129](https://github.com/puppetlabs/ruby-pwsh/pull/129) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
27
|
+
- \(MODULES-11051\) Ensure environment variables are not incorrectly munged in the PowerShell Host [\#128](https://github.com/puppetlabs/ruby-pwsh/pull/128) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
28
|
+
- \(MODULES-11026\) Ensure the PowerShell manager works with v7 [\#122](https://github.com/puppetlabs/ruby-pwsh/pull/122) ([n3snah](https://github.com/n3snah))
|
29
|
+
- \(Maint\) Ensure canonicalize correctly compares sorted hashes [\#118](https://github.com/puppetlabs/ruby-pwsh/pull/118) ([Hvid](https://github.com/Hvid))
|
30
|
+
|
31
|
+
## [0.8.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.8.0) (2021-03-01)
|
32
|
+
|
33
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.4...0.8.0)
|
34
|
+
|
13
35
|
## [0.7.4](https://github.com/puppetlabs/ruby-pwsh/tree/0.7.4) (2021-02-12)
|
14
36
|
|
15
37
|
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.3...0.7.4)
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -117,3 +117,15 @@ Steps to release an update to the gem and module include:
|
|
117
117
|
1. Submit the [mergeback PR from the release branch to main](https://github.com/puppetlabs/ruby-pwsh/compare/main...release).
|
118
118
|
|
119
119
|
## Known Issues
|
120
|
+
|
121
|
+
## Supported Operating Systems
|
122
|
+
|
123
|
+
The following platforms are supported:
|
124
|
+
|
125
|
+
- Windows
|
126
|
+
- CentOS
|
127
|
+
- Debian
|
128
|
+
- Fedora
|
129
|
+
- OSX
|
130
|
+
- RedHat
|
131
|
+
- Ubuntu
|
data/Rakefile
CHANGED
@@ -98,3 +98,33 @@ task :build_module do
|
|
98
98
|
# Cleanup
|
99
99
|
File.open('README.md', 'wb') { |file| file.write(actual_readme_content) }
|
100
100
|
end
|
101
|
+
|
102
|
+
namespace :dsc do
|
103
|
+
namespace :acceptance do
|
104
|
+
desc 'Prep for running DSC acceptance tests'
|
105
|
+
task :spec_prep do
|
106
|
+
# Create the modules fixture folder, if needed
|
107
|
+
modules_folder = File.expand_path('spec/fixtures/modules', File.dirname(__FILE__))
|
108
|
+
FileUtils.mkdir_p(modules_folder) unless Dir.exist?(modules_folder)
|
109
|
+
# symlink the parent folder to the modules folder for puppet
|
110
|
+
File.symlink(File.dirname(__FILE__), File.expand_path('pwshlib', modules_folder))
|
111
|
+
# Install each of the required modules for acceptance testing
|
112
|
+
# Note: This only works for modules in the dsc namespace on the forge.
|
113
|
+
[{ name: 'powershellget', version: '2.2.5-0-1' }].each do |puppet_module|
|
114
|
+
next if Dir.exist?(File.expand_path(puppet_module[:name], modules_folder))
|
115
|
+
|
116
|
+
install_command = [
|
117
|
+
'bundle exec puppet module install',
|
118
|
+
"dsc-#{puppet_module[:name]}",
|
119
|
+
"--version #{puppet_module[:version]}",
|
120
|
+
'--ignore-dependencies',
|
121
|
+
"--target-dir #{modules_folder}"
|
122
|
+
].join(' ')
|
123
|
+
run_local_command(install_command)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
127
|
+
t.pattern = 'spec/acceptance/dsc/*.rb'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -13,10 +13,15 @@ class Puppet::Provider::DscBaseProvider
|
|
13
13
|
def initialize
|
14
14
|
@@cached_canonicalized_resource ||= []
|
15
15
|
@@cached_query_results ||= []
|
16
|
+
@@cached_test_results ||= []
|
16
17
|
@@logon_failures ||= []
|
17
18
|
super
|
18
19
|
end
|
19
20
|
|
21
|
+
def cached_test_results
|
22
|
+
@@cached_test_results
|
23
|
+
end
|
24
|
+
|
20
25
|
# Look through a cache to retrieve the hashes specified, if they have been cached.
|
21
26
|
# Does so by seeing if each of the specified hashes is a subset of any of the hashes
|
22
27
|
# in the cache, so {foo: 1, bar: 2} would return if {foo: 1} was the search hash.
|
@@ -47,31 +52,50 @@ class Puppet::Provider::DscBaseProvider
|
|
47
52
|
# During RSAPI refresh runs mandatory parameters are stripped and not available;
|
48
53
|
# Instead of checking again and failing, search the cache for a namevar match.
|
49
54
|
namevarized_r = r.select { |k, _v| namevar_attributes(context).include?(k) }
|
50
|
-
|
51
|
-
|
52
|
-
|
55
|
+
cached_result = fetch_cached_hashes(@@cached_canonicalized_resource, [namevarized_r]).first
|
56
|
+
if cached_result.nil?
|
57
|
+
# If the resource is meant to be absent, skip canonicalization and rely on the manifest
|
58
|
+
# value; there's no reason to compare system state to desired state for casing if the
|
59
|
+
# resource is being removed.
|
60
|
+
if r[:dsc_ensure] == 'absent'
|
53
61
|
canonicalized = r.dup
|
54
62
|
@@cached_canonicalized_resource << r.dup
|
55
63
|
else
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
64
|
+
canonicalized = invoke_get_method(context, r)
|
65
|
+
# If the resource could not be found or was returned as absent, skip case munging and
|
66
|
+
# treat the manifest values as canonical since the resource is being created.
|
67
|
+
# rubocop:disable Metrics/BlockNesting
|
68
|
+
if canonicalized.nil? || canonicalized[:dsc_ensure] == 'absent'
|
69
|
+
canonicalized = r.dup
|
70
|
+
@@cached_canonicalized_resource << r.dup
|
61
71
|
else
|
62
|
-
|
72
|
+
parameters = r.select { |name, _properties| parameter_attributes(context).include?(name) }
|
73
|
+
canonicalized.merge!(parameters)
|
74
|
+
canonicalized[:name] = r[:name]
|
75
|
+
if r[:dsc_psdscrunascredential].nil?
|
76
|
+
canonicalized.delete(:dsc_psdscrunascredential)
|
77
|
+
else
|
78
|
+
canonicalized[:dsc_psdscrunascredential] = r[:dsc_psdscrunascredential]
|
79
|
+
end
|
80
|
+
downcased_result = recursively_downcase(canonicalized)
|
81
|
+
downcased_resource = recursively_downcase(r)
|
82
|
+
downcased_result.each do |key, value|
|
83
|
+
# Canonicalize to the manifest value unless the downcased strings match and the attribute is not an enum:
|
84
|
+
# - When the values don't match at all, the manifest value is desired;
|
85
|
+
# - When the values match case insensitively but the attribute is an enum, prefer the casing of the manifest enum.
|
86
|
+
# - When the values match case insensitively and the attribute is not an enum, prefer the casing from invoke_get_method
|
87
|
+
canonicalized[key] = r[key] unless same?(value, downcased_resource[key]) && !enum_attributes(context).include?(key)
|
88
|
+
canonicalized.delete(key) unless downcased_resource.keys.include?(key)
|
89
|
+
end
|
90
|
+
# Cache the actually canonicalized resource separately
|
91
|
+
@@cached_canonicalized_resource << canonicalized.dup
|
63
92
|
end
|
64
|
-
|
65
|
-
downcased_resource = recursively_downcase(r)
|
66
|
-
downcased_result.each do |key, value|
|
67
|
-
is_same = value.is_a?(Enumerable) & !downcased_resource[key].nil? ? downcased_resource[key].sort == value.sort : downcased_resource[key] == value
|
68
|
-
canonicalized[key] = r[key] unless is_same
|
69
|
-
canonicalized.delete(key) unless downcased_resource.keys.include?(key)
|
70
|
-
end
|
71
|
-
# Cache the actually canonicalized resource separately
|
72
|
-
@@cached_canonicalized_resource << canonicalized.dup
|
93
|
+
# rubocop:enable Metrics/BlockNesting
|
73
94
|
end
|
74
95
|
else
|
96
|
+
# The resource has already been canonicalized for the set values and is not being canonicalized for get
|
97
|
+
# In this case, we do *not* want to process anything, just return the resource. We only call canonicalize
|
98
|
+
# so we can get case insensitive but preserving values for _setting_ state.
|
75
99
|
canonicalized = r
|
76
100
|
end
|
77
101
|
canonicalized_resources << canonicalized
|
@@ -92,7 +116,7 @@ class Puppet::Provider::DscBaseProvider
|
|
92
116
|
# Relies on the get_simple_filter feature to pass the namevars
|
93
117
|
# as an array containing the namevar parameters as a hash.
|
94
118
|
# This hash is functionally the same as a should hash as
|
95
|
-
# passed to the
|
119
|
+
# passed to the invocable_resource method.
|
96
120
|
context.debug('Collecting data from the DSC Resource')
|
97
121
|
|
98
122
|
# If the resource has already been queried, do not bother querying for it again
|
@@ -123,27 +147,14 @@ class Puppet::Provider::DscBaseProvider
|
|
123
147
|
# @param changes [Hash] the hash of whose key is the name_hash and value is the is and should hashes
|
124
148
|
def set(context, changes)
|
125
149
|
changes.each do |name, change|
|
126
|
-
is = change
|
127
|
-
context.type.check_schema(is) unless change.key?(:is)
|
128
|
-
|
150
|
+
is = change[:is]
|
129
151
|
should = change[:should]
|
130
152
|
|
131
|
-
|
132
|
-
|
133
|
-
name_hash = {}
|
134
|
-
context.type.namevars.each do |namevar|
|
135
|
-
name_hash[namevar] = change[:should][namevar]
|
136
|
-
end
|
137
|
-
name_hash
|
138
|
-
else
|
139
|
-
name
|
140
|
-
end
|
153
|
+
# If should is an array instead of a hash and only has one entry, use that.
|
154
|
+
should = should.first if should.is_a?(Array) && should.length == 1
|
141
155
|
|
142
156
|
# for compatibility sake, we use dsc_ensure instead of ensure, so context.type.ensurable? does not work
|
143
157
|
if context.type.attributes.key?(:dsc_ensure)
|
144
|
-
is = create_absent(:name, name) if is.nil?
|
145
|
-
should = create_absent(:name, name) if should.nil?
|
146
|
-
|
147
158
|
# HACK: If the DSC Resource is ensurable but doesn't report a default value
|
148
159
|
# for ensure, we assume it to be `Present` - this is the most common pattern.
|
149
160
|
should_ensure = should[:dsc_ensure].nil? ? 'Present' : should[:dsc_ensure].to_s
|
@@ -152,47 +163,31 @@ class Puppet::Provider::DscBaseProvider
|
|
152
163
|
|
153
164
|
if is_ensure == 'Absent' && should_ensure == 'Present'
|
154
165
|
context.creating(name) do
|
155
|
-
create(context,
|
166
|
+
create(context, name, should)
|
156
167
|
end
|
157
168
|
elsif is_ensure == 'Present' && should_ensure == 'Present'
|
158
169
|
context.updating(name) do
|
159
|
-
update(context,
|
170
|
+
update(context, name, should)
|
160
171
|
end
|
161
172
|
elsif is_ensure == 'Present' && should_ensure == 'Absent'
|
162
173
|
context.deleting(name) do
|
163
|
-
delete(context,
|
174
|
+
delete(context, name)
|
164
175
|
end
|
165
176
|
else
|
166
177
|
# In this case we are not sure if the resource is being created/updated/removed
|
167
178
|
# as with ensure "latest" or a specific version number, so default to update.
|
168
179
|
context.updating(name) do
|
169
|
-
update(context,
|
180
|
+
update(context, name, should)
|
170
181
|
end
|
171
182
|
end
|
172
183
|
else
|
173
184
|
context.updating(name) do
|
174
|
-
update(context,
|
185
|
+
update(context, name, should)
|
175
186
|
end
|
176
187
|
end
|
177
188
|
end
|
178
189
|
end
|
179
190
|
|
180
|
-
# Creates a hash with the name / name_hash and sets dsc_ensure to absent for comparison
|
181
|
-
# purposes; this handles cases where the resource isn't found on the node.
|
182
|
-
#
|
183
|
-
# @param namevar [Object] the name of the variable being used for the resource name
|
184
|
-
# @param title [Hash] the hash of namevar properties and their values
|
185
|
-
# @return [Hash] returns a hash representing the absent state of the resource
|
186
|
-
def create_absent(namevar, title)
|
187
|
-
result = if title.is_a? Hash
|
188
|
-
title.dup
|
189
|
-
else
|
190
|
-
{ namevar => title }
|
191
|
-
end
|
192
|
-
result[:dsc_ensure] = 'Absent'
|
193
|
-
result
|
194
|
-
end
|
195
|
-
|
196
191
|
# Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
|
197
192
|
# the `invoke_set_method` method; whether this method, `update`, or `delete` is called is entirely
|
198
193
|
# up to the Resource API based on the results
|
@@ -229,32 +224,40 @@ class Puppet::Provider::DscBaseProvider
|
|
229
224
|
invoke_set_method(context, name, name.merge({ dsc_ensure: 'Absent' }))
|
230
225
|
end
|
231
226
|
|
232
|
-
# Invokes the
|
233
|
-
# The PowerShell script returns a JSON
|
234
|
-
#
|
235
|
-
# fit the expected property definitions. Finally, it returns the object for the Resource API to
|
236
|
-
# compare against and determine what future actions, if any, are needed.
|
227
|
+
# Invokes the given DSC method, passing the name_hash as the properties to use with `Invoke-DscResource`
|
228
|
+
# The PowerShell script returns a JSON hash with key-value pairs indicating the result of the given command.
|
229
|
+
# The hash is left untouched for the most part with any further parsing handled by the methods that call upon it.
|
237
230
|
#
|
238
231
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
239
232
|
# @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
|
240
|
-
# @
|
241
|
-
|
242
|
-
|
243
|
-
|
233
|
+
# @param props [Hash] the properties to be passed to `Invoke-DscResource`
|
234
|
+
# @param method [String] the method to be specified
|
235
|
+
# @return [Hash] returns a hash representing the result of the DSC resource call
|
236
|
+
def invoke_dsc_resource(context, name_hash, props, method)
|
244
237
|
# Do not bother running if the logon credentials won't work
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
238
|
+
if !name_hash[:dsc_psdscrunascredential].nil? && logon_failed_already?(name_hash[:dsc_psdscrunascredential])
|
239
|
+
context.err('Logon credentials are invalid')
|
240
|
+
return nil
|
241
|
+
end
|
242
|
+
resource = invocable_resource(props, context, method)
|
249
243
|
script_content = ps_script_content(resource)
|
250
244
|
context.debug("Script:\n #{redact_secrets(script_content)}")
|
251
|
-
output = ps_manager.execute(script_content)[:stdout]
|
252
|
-
|
245
|
+
output = ps_manager.execute(remove_secret_identifiers(script_content))[:stdout]
|
246
|
+
if output.nil?
|
247
|
+
context.err('Nothing returned')
|
248
|
+
return nil
|
249
|
+
end
|
253
250
|
|
254
|
-
|
251
|
+
begin
|
252
|
+
data = JSON.parse(output)
|
253
|
+
rescue => e
|
254
|
+
context.err(e)
|
255
|
+
return nil
|
256
|
+
end
|
255
257
|
context.debug("raw data received: #{data.inspect}")
|
258
|
+
|
256
259
|
error = data['errormessage']
|
257
|
-
unless error.nil?
|
260
|
+
unless error.nil? || error.empty?
|
258
261
|
# NB: We should have a way to stop processing this resource *now* without blowing up the whole Puppet run
|
259
262
|
# Raising an error stops processing but blows things up while context.err alerts but continues to process
|
260
263
|
if error =~ /Logon failure: the user has not been granted the requested logon type at this computer/
|
@@ -269,10 +272,47 @@ class Puppet::Provider::DscBaseProvider
|
|
269
272
|
# Either way, something went wrong and we didn't get back a good result, so return nil
|
270
273
|
return nil
|
271
274
|
end
|
275
|
+
data
|
276
|
+
end
|
277
|
+
|
278
|
+
# Determine if the DSC Resource is in the desired state, invoking the `Test` method unless it's
|
279
|
+
# already been run for the resource, in which case reuse the result instead of checking for each
|
280
|
+
# property. This behavior is only triggered if the validation_mode is set to resource; by default
|
281
|
+
# it is set to property and uses the default property comparison logic in Puppet::Property.
|
282
|
+
#
|
283
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
284
|
+
# @param name [String] the name of the resource being tested
|
285
|
+
# @param is_hash [Hash] the current state of the resource on the system
|
286
|
+
# @param should_hash [Hash] the desired state of the resource per the manifest
|
287
|
+
# @return [Boolean, Void] returns true/false if the resource is/isn't in the desired state and
|
288
|
+
# the validation mode is set to resource, otherwise nil.
|
289
|
+
def insync?(context, name, _property_name, _is_hash, should_hash)
|
290
|
+
return nil if should_hash[:validation_mode] != 'resource'
|
291
|
+
|
292
|
+
prior_result = fetch_cached_hashes(@@cached_test_results, [name])
|
293
|
+
prior_result.empty? ? invoke_test_method(context, name, should_hash) : prior_result.first[:in_desired_state]
|
294
|
+
end
|
295
|
+
|
296
|
+
# Invokes the `Get` method, passing the name_hash as the properties to use with `Invoke-DscResource`
|
297
|
+
# The PowerShell script returns a JSON representation of the DSC Resource's CIM Instance munged as
|
298
|
+
# best it can be for Ruby. Once that JSON is parsed into a hash this method further munges it to
|
299
|
+
# fit the expected property definitions. Finally, it returns the object for the Resource API to
|
300
|
+
# compare against and determine what future actions, if any, are needed.
|
301
|
+
#
|
302
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
303
|
+
# @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
|
304
|
+
# @return [Hash] returns a hash representing the DSC resource munged to the representation the Puppet Type expects
|
305
|
+
def invoke_get_method(context, name_hash)
|
306
|
+
context.debug("retrieving #{name_hash.inspect}")
|
307
|
+
|
308
|
+
query_props = name_hash.select { |k, v| mandatory_get_attributes(context).include?(k) || (k == :dsc_psdscrunascredential && !v.nil?) }
|
309
|
+
data = invoke_dsc_resource(context, name_hash, query_props, 'get')
|
310
|
+
return nil if data.nil?
|
311
|
+
|
272
312
|
# DSC gives back information we don't care about; filter down to only
|
273
313
|
# those properties exposed in the type definition.
|
274
314
|
valid_attributes = context.type.attributes.keys.collect(&:to_s)
|
275
|
-
parameters = context
|
315
|
+
parameters = parameter_attributes(context).collect(&:to_s)
|
276
316
|
data.select! { |key, _value| valid_attributes.include?("dsc_#{key.downcase}") }
|
277
317
|
data.reject! { |key, _value| parameters.include?("dsc_#{key.downcase}") }
|
278
318
|
# Canonicalize the results to match the type definition representation;
|
@@ -281,14 +321,26 @@ class Puppet::Provider::DscBaseProvider
|
|
281
321
|
data.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
282
322
|
type_key = "dsc_#{key.downcase}".to_sym
|
283
323
|
data[type_key] = data.delete(key)
|
284
|
-
|
324
|
+
|
325
|
+
# Special handling for CIM Instances
|
326
|
+
if data[type_key].is_a?(Enumerable)
|
327
|
+
downcase_hash_keys!(data[type_key])
|
328
|
+
munge_cim_instances!(data[type_key])
|
329
|
+
end
|
330
|
+
|
285
331
|
# Convert DateTime back to appropriate type
|
286
|
-
|
332
|
+
if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil?
|
333
|
+
data[type_key] = begin
|
334
|
+
Puppet::Pops::Time::Timestamp.parse(data[type_key]) if context.type.attributes[type_key][:mof_type] =~ /DateTime/i && !data[type_key].nil?
|
335
|
+
rescue ArgumentError, TypeError => e
|
336
|
+
# Catch any failures in the parse, output them to the context and then return nil
|
337
|
+
context.err("Value returned for DateTime (#{data[type_key].inspect}) failed to parse: #{e}")
|
338
|
+
nil
|
339
|
+
end
|
340
|
+
end
|
287
341
|
# PowerShell does not distinguish between a return of empty array/string
|
288
342
|
# and null but Puppet does; revert to those values if specified.
|
289
|
-
if data[type_key].nil? && query_props.keys.include?(type_key) && query_props[type_key].is_a?(Array)
|
290
|
-
data[type_key] = query_props[type_key].empty? ? query_props[type_key] : []
|
291
|
-
end
|
343
|
+
data[type_key] = [] if data[type_key].nil? && query_props.keys.include?(type_key) && query_props[type_key].is_a?(Array)
|
292
344
|
end
|
293
345
|
# If a resource is found, it's present, so refill this Puppet-only key
|
294
346
|
data.merge!({ name: name_hash[:name] })
|
@@ -302,6 +354,9 @@ class Puppet::Provider::DscBaseProvider
|
|
302
354
|
# declaration is for an absent resource and the resource is actually absent
|
303
355
|
data.reject! { |_k, v| v.nil? } if data[:dsc_ensure] == 'Absent' && name_hash[:dsc_ensure] == 'Absent' && !name_hash_has_nil_keys
|
304
356
|
|
357
|
+
# Sort the return for order-insensitive nested enumerable comparison:
|
358
|
+
data = recursively_sort(data)
|
359
|
+
|
305
360
|
# Cache the query to prevent a second lookup
|
306
361
|
@@cached_query_results << data.dup if fetch_cached_hashes(@@cached_query_results, [data]).empty?
|
307
362
|
context.debug("Returned to Puppet as #{data}")
|
@@ -318,24 +373,36 @@ class Puppet::Provider::DscBaseProvider
|
|
318
373
|
def invoke_set_method(context, name, should)
|
319
374
|
context.debug("Invoking Set Method for '#{name}' with #{should.inspect}")
|
320
375
|
|
321
|
-
# Do not bother running if the logon credentials won't work
|
322
|
-
return nil if !should[:dsc_psdscrunascredential].nil? && logon_failed_already?(should[:dsc_psdscrunascredential])
|
323
|
-
|
324
376
|
apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
|
325
|
-
|
326
|
-
script_content = ps_script_content(resource)
|
327
|
-
context.debug("Script:\n #{redact_secrets(script_content)}")
|
328
|
-
|
329
|
-
output = ps_manager.execute(script_content)[:stdout]
|
330
|
-
context.err('Nothing returned') if output.nil?
|
377
|
+
invoke_dsc_resource(context, should, apply_props, 'set')
|
331
378
|
|
332
|
-
data = JSON.parse(output)
|
333
|
-
context.debug(data)
|
334
|
-
|
335
|
-
context.err(data['errormessage']) unless data['errormessage'].empty?
|
336
379
|
# TODO: Implement this functionality for notifying a DSC reboot?
|
337
380
|
# notify_reboot_pending if data['rebootrequired'] == true
|
338
|
-
|
381
|
+
end
|
382
|
+
|
383
|
+
# Invokes the `Test` method, passing the should hash as the properties to use with `Invoke-DscResource`
|
384
|
+
# The PowerShell script returns a JSON hash with key-value pairs indicating whether or not the resource
|
385
|
+
# is in the desired state and any error messages captured.
|
386
|
+
#
|
387
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
388
|
+
# @param should [Hash] the desired state represented definition to pass as properties to Invoke-DscResource
|
389
|
+
# @return [Boolean] returns true if the resource is in the desired state, otherwise false
|
390
|
+
def invoke_test_method(context, name, should)
|
391
|
+
context.debug("Relying on DSC Test method for validating if '#{name}' is in the desired state")
|
392
|
+
context.debug("Invoking Test Method for '#{name}' with #{should.inspect}")
|
393
|
+
|
394
|
+
test_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
|
395
|
+
data = invoke_dsc_resource(context, name, test_props, 'test')
|
396
|
+
# Something went wrong with Invoke-DscResource; fall back on property state comparisons
|
397
|
+
return nil if data.nil?
|
398
|
+
|
399
|
+
in_desired_state = data['indesiredstate']
|
400
|
+
@@cached_test_results << name.merge({ in_desired_state: in_desired_state })
|
401
|
+
|
402
|
+
return in_desired_state if in_desired_state
|
403
|
+
|
404
|
+
change_log = 'DSC reported that this resource is not in the desired state; treating all properties as out-of-sync'
|
405
|
+
[in_desired_state, change_log]
|
339
406
|
end
|
340
407
|
|
341
408
|
# Converts a Puppet resource hash into a hash with the information needed to call Invoke-DscResource,
|
@@ -346,7 +413,7 @@ class Puppet::Provider::DscBaseProvider
|
|
346
413
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
347
414
|
# @param dsc_invoke_method [String] the method to pass to Invoke-DscResource: get, set, or test
|
348
415
|
# @return [Hash] a hash with the information needed to run `Invoke-DscResource`
|
349
|
-
def
|
416
|
+
def invocable_resource(should, context, dsc_invoke_method)
|
350
417
|
resource = {}
|
351
418
|
resource[:parameters] = {}
|
352
419
|
%i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_module_name dscmeta_module_version].each do |k|
|
@@ -365,6 +432,15 @@ class Puppet::Provider::DscBaseProvider
|
|
365
432
|
end
|
366
433
|
resource[:dsc_invoke_method] = dsc_invoke_method
|
367
434
|
|
435
|
+
resource[:vendored_modules_path] = vendored_modules_path(resource[:dscmeta_module_name])
|
436
|
+
|
437
|
+
resource[:attributes] = nil
|
438
|
+
|
439
|
+
context.debug("invocable_resource: #{resource.inspect}")
|
440
|
+
resource
|
441
|
+
end
|
442
|
+
|
443
|
+
def vendored_modules_path(module_name)
|
368
444
|
# Because Puppet adds all of the modules to the LOAD_PATH we can be sure that the appropriate module lives here during an apply;
|
369
445
|
# PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
|
370
446
|
# During a Puppet agent run, the code lives in the cache so we can use the file expansion to discover the correct folder.
|
@@ -372,36 +448,41 @@ class Puppet::Provider::DscBaseProvider
|
|
372
448
|
# path to allow multiple modules to with shared dsc_resources to be installed side by side
|
373
449
|
# The old vendored_modules_path: puppet_x/dsc_resources
|
374
450
|
# The new vendored_modules_path: puppet_x/<module_name>/dsc_resources
|
375
|
-
root_module_path =
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
451
|
+
root_module_path = load_path.select { |path| path.match?(%r{#{puppetize_name(module_name)}/lib}) }.first
|
452
|
+
vendored_path = if root_module_path.nil?
|
453
|
+
File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + "puppet_x/#{puppetize_name(module_name)}/dsc_resources") # rubocop:disable Style/StringConcatenation
|
454
|
+
else
|
455
|
+
File.expand_path("#{root_module_path}/puppet_x/#{puppetize_name(module_name)}/dsc_resources")
|
456
|
+
end
|
381
457
|
|
382
458
|
# Check for the old vendored_modules_path second - if there is a mix of modules with the old and new pathing,
|
383
459
|
# checking for this first will always work and so the more specific search will never run.
|
384
|
-
unless File.exist?
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
460
|
+
unless File.exist? vendored_path
|
461
|
+
vendored_path = if root_module_path.nil?
|
462
|
+
File.expand_path(Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/dsc_resources') # rubocop:disable Style/StringConcatenation
|
463
|
+
else
|
464
|
+
File.expand_path("#{root_module_path}/puppet_x/dsc_resources")
|
465
|
+
end
|
390
466
|
end
|
391
467
|
|
392
468
|
# A warning is thrown if the something went wrong and the file was not created
|
393
|
-
raise "Unable to find expected vendored DSC Resource #{
|
469
|
+
raise "Unable to find expected vendored DSC Resource #{vendored_path}" unless File.exist? vendored_path
|
394
470
|
|
395
|
-
|
471
|
+
vendored_path
|
472
|
+
end
|
396
473
|
|
397
|
-
|
398
|
-
|
474
|
+
# Return the ruby $LOAD_PATH variable; this method exists to make testing vendored
|
475
|
+
# resource path discovery easier.
|
476
|
+
#
|
477
|
+
# @return [Array] The absolute file paths to available/known ruby code paths
|
478
|
+
def load_path
|
479
|
+
$LOAD_PATH
|
399
480
|
end
|
400
481
|
|
401
482
|
# Return a String containing a puppetized name. A puppetized name is a string that only
|
402
483
|
# includes lowercase letters, digits, underscores and cannot start with a digit.
|
403
484
|
#
|
404
|
-
# @return [String] with a
|
485
|
+
# @return [String] with a puppetized module name
|
405
486
|
def puppetize_name(name)
|
406
487
|
# Puppet module names must be lower case
|
407
488
|
name = name.downcase
|
@@ -444,20 +525,27 @@ class Puppet::Provider::DscBaseProvider
|
|
444
525
|
end
|
445
526
|
end
|
446
527
|
|
447
|
-
# Recursively transforms any enumerable,
|
528
|
+
# Recursively transforms any enumerable, downcasing any hash keys it finds, changing the passed enumerable.
|
448
529
|
#
|
449
530
|
# @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
|
450
|
-
|
451
|
-
def camelcase_hash_keys!(enumerable)
|
531
|
+
def downcase_hash_keys!(enumerable)
|
452
532
|
if enumerable.is_a?(Hash)
|
453
533
|
enumerable.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
454
|
-
name = key.dup
|
455
|
-
name[0] = name[0].downcase
|
534
|
+
name = key.dup.downcase
|
456
535
|
enumerable[name] = enumerable.delete(key)
|
457
|
-
|
536
|
+
downcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
|
458
537
|
end
|
459
538
|
else
|
460
|
-
enumerable.each { |item|
|
539
|
+
enumerable.each { |item| downcase_hash_keys!(item) if item.is_a?(Enumerable) }
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
def munge_cim_instances!(enumerable)
|
544
|
+
if enumerable.is_a?(Hash)
|
545
|
+
# Delete the cim_instance_type key from a top-level CIM Instance **only**
|
546
|
+
_ = enumerable.delete('cim_instance_type')
|
547
|
+
else
|
548
|
+
enumerable.each { |item| munge_cim_instances!(item) if item.is_a?(Enumerable) }
|
461
549
|
end
|
462
550
|
end
|
463
551
|
|
@@ -482,6 +570,34 @@ class Puppet::Provider::DscBaseProvider
|
|
482
570
|
end
|
483
571
|
end
|
484
572
|
|
573
|
+
# Recursively sorts any object to enable order-insensitive comparisons
|
574
|
+
#
|
575
|
+
# @param object [Object] an array, hash, or other object to attempt to recursively downcase
|
576
|
+
# @return [Object] returns the input object recursively downcased
|
577
|
+
def recursively_sort(object)
|
578
|
+
case object
|
579
|
+
when Array
|
580
|
+
object.map { |item| recursively_sort(item) }.sort_by(&:to_s)
|
581
|
+
when Hash
|
582
|
+
transformed = {}
|
583
|
+
object.sort.to_h.each do |key, value|
|
584
|
+
transformed[key] = recursively_sort(value)
|
585
|
+
end
|
586
|
+
transformed
|
587
|
+
else
|
588
|
+
object
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
# Check equality, sort if necessary
|
593
|
+
#
|
594
|
+
# @param value1 [object] a string, array, hash, or other object to sort and compare to value2
|
595
|
+
# @param value2 [object] a string, array, hash, or other object to sort and compare to value1
|
596
|
+
# @return [bool] returns equality
|
597
|
+
def same?(value1, value2)
|
598
|
+
recursively_sort(value2) == recursively_sort(value1)
|
599
|
+
end
|
600
|
+
|
485
601
|
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
|
486
602
|
#
|
487
603
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
@@ -514,6 +630,16 @@ class Puppet::Provider::DscBaseProvider
|
|
514
630
|
context.type.attributes.select { |_name, properties| properties[:behaviour] == :parameter }.keys
|
515
631
|
end
|
516
632
|
|
633
|
+
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as enums
|
634
|
+
# Note that for complex types, especially those that have nested CIM instances, this will return for any data
|
635
|
+
# type which *includes* an Enum, not just for simple `Enum[]` or `Optional[Enum[]]` data types.
|
636
|
+
#
|
637
|
+
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
638
|
+
# @return [Array] returns an array of attribute names as symbols which are enums
|
639
|
+
def enum_attributes(context)
|
640
|
+
context.type.attributes.select { |_name, properties| properties[:type].match(/Enum\[/) }.keys
|
641
|
+
end
|
642
|
+
|
517
643
|
# Look through a fully formatted string, replacing all instances where a value matches the formatted properties
|
518
644
|
# of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
|
519
645
|
# CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
|
@@ -529,7 +655,7 @@ class Puppet::Provider::DscBaseProvider
|
|
529
655
|
modified_string
|
530
656
|
end
|
531
657
|
|
532
|
-
# Parses a resource definition (as from `
|
658
|
+
# Parses a resource definition (as from `invocable_resource`) for any properties which are PowerShell
|
533
659
|
# Credentials. As these values need to be serialized into PSCredential objects, return an array of
|
534
660
|
# PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
|
535
661
|
# These credential variables can then be simply assigned in the parameter hash where needed.
|
@@ -560,10 +686,10 @@ class Puppet::Provider::DscBaseProvider
|
|
560
686
|
# @param credential_hash [Hash] the Properties which define the PSCredential Object
|
561
687
|
# @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
|
562
688
|
def format_pscredential(variable_name, credential_hash)
|
563
|
-
"$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}'
|
689
|
+
"$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}#{SECRET_POSTFIX}'"
|
564
690
|
end
|
565
691
|
|
566
|
-
# Parses a resource definition (as from `
|
692
|
+
# Parses a resource definition (as from `invocable_resource`) for any properties which are CIM instances
|
567
693
|
# whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
|
568
694
|
# those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
|
569
695
|
# will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
|
@@ -644,7 +770,7 @@ class Puppet::Provider::DscBaseProvider
|
|
644
770
|
interpolate_variables(definition)
|
645
771
|
end
|
646
772
|
|
647
|
-
# Munge a resource definition (as from `
|
773
|
+
# Munge a resource definition (as from `invocable_resource`) into valid PowerShell which represents
|
648
774
|
# the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
|
649
775
|
# defined variables into the hash.
|
650
776
|
#
|
@@ -700,7 +826,7 @@ class Puppet::Provider::DscBaseProvider
|
|
700
826
|
params_block
|
701
827
|
end
|
702
828
|
|
703
|
-
# Given a resource definition (as from `
|
829
|
+
# Given a resource definition (as from `invocable_resource`), return a PowerShell script which has
|
704
830
|
# all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
|
705
831
|
# will correct munge the results for returning to Puppet as a JSON object.
|
706
832
|
#
|
@@ -735,19 +861,18 @@ class Puppet::Provider::DscBaseProvider
|
|
735
861
|
rescue RuntimeError => e
|
736
862
|
raise unless e.message =~ /Sensitive \[value redacted\]/
|
737
863
|
|
738
|
-
|
739
|
-
string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
|
864
|
+
Pwsh::Util.format_powershell_value(unwrap(value))
|
740
865
|
end
|
741
866
|
|
742
|
-
# Unwrap sensitive strings for formatting, even inside an enumerable, appending
|
743
|
-
# to the end of the string in preparation for gsub cleanup.
|
867
|
+
# Unwrap sensitive strings for formatting, even inside an enumerable, appending the
|
868
|
+
# the secret postfix to the end of the string in preparation for gsub cleanup.
|
744
869
|
#
|
745
870
|
# @param value [Object] The object to unwrap sensitive data inside of
|
746
871
|
# @return [Object] The object with any sensitive strings unwrapped and annotated
|
747
872
|
def unwrap(value)
|
748
873
|
case value
|
749
874
|
when Puppet::Pops::Types::PSensitiveType::Sensitive
|
750
|
-
"#{value.unwrap}#
|
875
|
+
"#{value.unwrap}#{SECRET_POSTFIX}"
|
751
876
|
when Hash
|
752
877
|
unwrapped = {}
|
753
878
|
value.each do |k, v|
|
@@ -773,6 +898,47 @@ class Puppet::Provider::DscBaseProvider
|
|
773
898
|
text.gsub("'", "''")
|
774
899
|
end
|
775
900
|
|
901
|
+
# In order to avoid having to update the string that indicates when a value came from a sensitive
|
902
|
+
# string in multiple places, use a constant to indicate what the text of the secret identifier
|
903
|
+
# should be. This is used to write, identify, and redact secrets between PowerShell & Puppet.
|
904
|
+
SECRET_POSTFIX = '#PuppetSensitive'
|
905
|
+
|
906
|
+
# With multiple methods which need to discover secrets it is necessary to keep a single regex
|
907
|
+
# which can discover them. This will lazily match everything in a single-quoted string which
|
908
|
+
# ends with the secret postfix id and mark the actual contents of the string as the secret.
|
909
|
+
SECRET_DATA_REGEX = /'(?<secret>[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'/.freeze
|
910
|
+
|
911
|
+
# Strings containing sensitive data have a secrets postfix. These strings cannot be passed
|
912
|
+
# directly either to debug streams or to PowerShell and must be handled; this method contains
|
913
|
+
# the shared logic for parsing text for secrets and substituting values for them.
|
914
|
+
#
|
915
|
+
# @param text [String] the text to parse and handle for secrets
|
916
|
+
# @param replacement [String] the value to pass to gsub to replace secrets with
|
917
|
+
# @param error_message [String] the error message to raise instead of leaking secrets
|
918
|
+
# @return [String] the modified text
|
919
|
+
def handle_secrets(text, replacement, error_message)
|
920
|
+
# Every secret unwrapped in this module will unwrap as "'secret#{SECRET_POSTFIX}'"
|
921
|
+
# Currently, no known resources specify a SecureString instead of a PSCredential object.
|
922
|
+
return text unless text.match(/#{Regexp.quote(SECRET_POSTFIX)}/)
|
923
|
+
|
924
|
+
# In order to reduce time-to-parse, look at each line individually and *only* attempt
|
925
|
+
# to substitute if a naive match for the secret postfix is found on the line.
|
926
|
+
modified_text = text.split("\n").map do |line|
|
927
|
+
if line.match(/#{Regexp.quote(SECRET_POSTFIX)}/)
|
928
|
+
line.gsub(SECRET_DATA_REGEX, replacement)
|
929
|
+
else
|
930
|
+
line
|
931
|
+
end
|
932
|
+
end
|
933
|
+
|
934
|
+
modified_text = modified_text.join("\n")
|
935
|
+
|
936
|
+
# Something has gone wrong, error loudly
|
937
|
+
raise error_message if modified_text =~ /#{Regexp.quote(SECRET_POSTFIX)}/
|
938
|
+
|
939
|
+
modified_text
|
940
|
+
end
|
941
|
+
|
776
942
|
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
777
943
|
# and so for debugging purposes must be redacted before being sent to debug
|
778
944
|
# output but must *not* be redacted when sent to the PowerShell code manager.
|
@@ -780,15 +946,17 @@ class Puppet::Provider::DscBaseProvider
|
|
780
946
|
# @param text [String] the text to redact
|
781
947
|
# @return [String] the redacted text
|
782
948
|
def redact_secrets(text)
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
949
|
+
handle_secrets(text, "'#<Sensitive [value redacted]>'", "Unredacted sensitive data would've been leaked")
|
950
|
+
end
|
951
|
+
|
952
|
+
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
953
|
+
# and so the helper-id for sensitive data *must* be removed before sending to
|
954
|
+
# the PowerShell code manager.
|
955
|
+
#
|
956
|
+
# @param text [String] the text to strip of secret data identifiers
|
957
|
+
# @return [String] the modified text to pass to the PowerShell code manager
|
958
|
+
def remove_secret_identifiers(text)
|
959
|
+
handle_secrets(text, "'\\k<secret>'", 'Unable to properly format text for PowerShell with sensitive data')
|
792
960
|
end
|
793
961
|
|
794
962
|
# Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
|