ruby-pwsh 0.7.2 → 0.10.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 +55 -0
- data/Gemfile +5 -1
- data/README.md +12 -0
- data/Rakefile +34 -0
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +342 -139
- data/lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_functions.ps1 +5 -3
- data/lib/puppet/provider/dsc_base_provider/invoke_dsc_resource_postscript.ps1 +6 -0
- 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 +8 -9
- 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: 350652f7e3803a522f6ad56512a75778277bef24bb3963fa9713522517f511cc
|
4
|
+
data.tar.gz: '02500121972f8b43463d6e3db9d37ab7324bcecb521b0fc482a9540d1c7bb88b'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5f3e181119fc56713f3f326081ec0f84540308f8918425afc7b10e89baa6813456b3e369b98b7c7ae255484ba425dee0e00239cce5d98683b013d8971ad5d27a
|
7
|
+
data.tar.gz: 0eaa28aeb753b121d46cd83127ff0dffdaa4cbf8bb0463b07b069fea0a339461a71d1746232e9cc3ad49413daa07b315c0a11b55eb85700f54bcda5667606db4
|
@@ -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,6 +2,61 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
+
## [0.10.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.10.0) (2021-07-01)
|
6
|
+
|
7
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.9.0...0.10.0)
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
- \(GH-172\) Enable use of class-based DSC Resources by munging PSModulePath [\#173](https://github.com/puppetlabs/ruby-pwsh/pull/173) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
12
|
+
|
13
|
+
## [0.9.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.9.0) (2021-06-28)
|
14
|
+
|
15
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.8.0...0.9.0)
|
16
|
+
|
17
|
+
### Added
|
18
|
+
|
19
|
+
- \(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))
|
20
|
+
- \(GH-145\) Improve DSC secrets redaction [\#150](https://github.com/puppetlabs/ruby-pwsh/pull/150) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
21
|
+
- \(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))
|
22
|
+
- \(MAINT\) Clarify supported platforms [\#113](https://github.com/puppetlabs/ruby-pwsh/pull/113) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
|
26
|
+
- \(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))
|
27
|
+
- \(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))
|
28
|
+
- \(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))
|
29
|
+
- \(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))
|
30
|
+
- \(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))
|
31
|
+
- \(GH-144\) Enable order-insensitive comparisons for DSC [\#151](https://github.com/puppetlabs/ruby-pwsh/pull/151) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
32
|
+
- \(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))
|
33
|
+
- \(GH-127\) Canonicalize enums correctly [\#131](https://github.com/puppetlabs/ruby-pwsh/pull/131) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
34
|
+
- \(GH-125\) Fix dsc provider canonicalization for absent resources [\#129](https://github.com/puppetlabs/ruby-pwsh/pull/129) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
35
|
+
- \(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))
|
36
|
+
- \(MODULES-11026\) Ensure the PowerShell manager works with v7 [\#122](https://github.com/puppetlabs/ruby-pwsh/pull/122) ([n3snah](https://github.com/n3snah))
|
37
|
+
- \(Maint\) Ensure canonicalize correctly compares sorted hashes [\#118](https://github.com/puppetlabs/ruby-pwsh/pull/118) ([Hvid](https://github.com/Hvid))
|
38
|
+
|
39
|
+
## [0.8.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.8.0) (2021-03-01)
|
40
|
+
|
41
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.4...0.8.0)
|
42
|
+
|
43
|
+
## [0.7.4](https://github.com/puppetlabs/ruby-pwsh/tree/0.7.4) (2021-02-12)
|
44
|
+
|
45
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.3...0.7.4)
|
46
|
+
|
47
|
+
### Fixed
|
48
|
+
|
49
|
+
- \(GH-105\) Ensure set runs on ambiguous ensure states [\#108](https://github.com/puppetlabs/ruby-pwsh/pull/108) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
50
|
+
- \(GH-105\) Ensure canonicalized\_cache check validates against namevar [\#107](https://github.com/puppetlabs/ruby-pwsh/pull/107) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
51
|
+
|
52
|
+
## [0.7.3](https://github.com/puppetlabs/ruby-pwsh/tree/0.7.3) (2021-02-03)
|
53
|
+
|
54
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.2...0.7.3)
|
55
|
+
|
56
|
+
### Fixed
|
57
|
+
|
58
|
+
- \(MAINT\) Place nil check when assigning is\_same [\#101](https://github.com/puppetlabs/ruby-pwsh/pull/101) ([bwilcox](https://github.com/bwilcox))
|
59
|
+
|
5
60
|
## [0.7.2](https://github.com/puppetlabs/ruby-pwsh/tree/0.7.2) (2021-02-03)
|
6
61
|
|
7
62
|
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.1...0.7.2)
|
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,37 @@ 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
|
+
puppetized_dsc_modules = [
|
114
|
+
{ name: 'powershellget', version: '2.2.5-0-1' },
|
115
|
+
{ name: 'jeadsc', version: '0.7.2-0-2' } # update to 0.7.2-0-3 on release
|
116
|
+
]
|
117
|
+
puppetized_dsc_modules.each do |puppet_module|
|
118
|
+
next if Dir.exist?(File.expand_path(puppet_module[:name], modules_folder))
|
119
|
+
|
120
|
+
install_command = [
|
121
|
+
'bundle exec puppet module install',
|
122
|
+
"dsc-#{puppet_module[:name]}",
|
123
|
+
"--version #{puppet_module[:version]}",
|
124
|
+
'--ignore-dependencies',
|
125
|
+
"--target-dir #{modules_folder}"
|
126
|
+
].join(' ')
|
127
|
+
run_local_command(install_command)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
131
|
+
t.pattern = 'spec/acceptance/dsc/*.rb'
|
132
|
+
end
|
133
|
+
end
|
134
|
+
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.
|
@@ -44,31 +49,53 @@ class Puppet::Provider::DscBaseProvider
|
|
44
49
|
def canonicalize(context, resources)
|
45
50
|
canonicalized_resources = []
|
46
51
|
resources.collect do |r|
|
47
|
-
|
48
|
-
|
49
|
-
|
52
|
+
# During RSAPI refresh runs mandatory parameters are stripped and not available;
|
53
|
+
# Instead of checking again and failing, search the cache for a namevar match.
|
54
|
+
namevarized_r = r.select { |k, _v| namevar_attributes(context).include?(k) }
|
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'
|
50
61
|
canonicalized = r.dup
|
51
62
|
@@cached_canonicalized_resource << r.dup
|
52
63
|
else
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
58
71
|
else
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
67
92
|
end
|
68
|
-
#
|
69
|
-
@@cached_canonicalized_resource << canonicalized.dup
|
93
|
+
# rubocop:enable Metrics/BlockNesting
|
70
94
|
end
|
71
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.
|
72
99
|
canonicalized = r
|
73
100
|
end
|
74
101
|
canonicalized_resources << canonicalized
|
@@ -89,7 +116,7 @@ class Puppet::Provider::DscBaseProvider
|
|
89
116
|
# Relies on the get_simple_filter feature to pass the namevars
|
90
117
|
# as an array containing the namevar parameters as a hash.
|
91
118
|
# This hash is functionally the same as a should hash as
|
92
|
-
# passed to the
|
119
|
+
# passed to the invocable_resource method.
|
93
120
|
context.debug('Collecting data from the DSC Resource')
|
94
121
|
|
95
122
|
# If the resource has already been queried, do not bother querying for it again
|
@@ -120,27 +147,14 @@ class Puppet::Provider::DscBaseProvider
|
|
120
147
|
# @param changes [Hash] the hash of whose key is the name_hash and value is the is and should hashes
|
121
148
|
def set(context, changes)
|
122
149
|
changes.each do |name, change|
|
123
|
-
is = change
|
124
|
-
context.type.check_schema(is) unless change.key?(:is)
|
125
|
-
|
150
|
+
is = change[:is]
|
126
151
|
should = change[:should]
|
127
152
|
|
128
|
-
|
129
|
-
|
130
|
-
name_hash = {}
|
131
|
-
context.type.namevars.each do |namevar|
|
132
|
-
name_hash[namevar] = change[:should][namevar]
|
133
|
-
end
|
134
|
-
name_hash
|
135
|
-
else
|
136
|
-
name
|
137
|
-
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
|
138
155
|
|
139
156
|
# for compatibility sake, we use dsc_ensure instead of ensure, so context.type.ensurable? does not work
|
140
157
|
if context.type.attributes.key?(:dsc_ensure)
|
141
|
-
is = create_absent(:name, name) if is.nil?
|
142
|
-
should = create_absent(:name, name) if should.nil?
|
143
|
-
|
144
158
|
# HACK: If the DSC Resource is ensurable but doesn't report a default value
|
145
159
|
# for ensure, we assume it to be `Present` - this is the most common pattern.
|
146
160
|
should_ensure = should[:dsc_ensure].nil? ? 'Present' : should[:dsc_ensure].to_s
|
@@ -149,41 +163,31 @@ class Puppet::Provider::DscBaseProvider
|
|
149
163
|
|
150
164
|
if is_ensure == 'Absent' && should_ensure == 'Present'
|
151
165
|
context.creating(name) do
|
152
|
-
create(context,
|
166
|
+
create(context, name, should)
|
153
167
|
end
|
154
168
|
elsif is_ensure == 'Present' && should_ensure == 'Present'
|
155
169
|
context.updating(name) do
|
156
|
-
update(context,
|
170
|
+
update(context, name, should)
|
157
171
|
end
|
158
172
|
elsif is_ensure == 'Present' && should_ensure == 'Absent'
|
159
173
|
context.deleting(name) do
|
160
|
-
delete(context,
|
174
|
+
delete(context, name)
|
175
|
+
end
|
176
|
+
else
|
177
|
+
# In this case we are not sure if the resource is being created/updated/removed
|
178
|
+
# as with ensure "latest" or a specific version number, so default to update.
|
179
|
+
context.updating(name) do
|
180
|
+
update(context, name, should)
|
161
181
|
end
|
162
182
|
end
|
163
183
|
else
|
164
184
|
context.updating(name) do
|
165
|
-
update(context,
|
185
|
+
update(context, name, should)
|
166
186
|
end
|
167
187
|
end
|
168
188
|
end
|
169
189
|
end
|
170
190
|
|
171
|
-
# Creates a hash with the name / name_hash and sets dsc_ensure to absent for comparison
|
172
|
-
# purposes; this handles cases where the resource isn't found on the node.
|
173
|
-
#
|
174
|
-
# @param namevar [Object] the name of the variable being used for the resource name
|
175
|
-
# @param title [Hash] the hash of namevar properties and their values
|
176
|
-
# @return [Hash] returns a hash representing the absent state of the resource
|
177
|
-
def create_absent(namevar, title)
|
178
|
-
result = if title.is_a? Hash
|
179
|
-
title.dup
|
180
|
-
else
|
181
|
-
{ namevar => title }
|
182
|
-
end
|
183
|
-
result[:dsc_ensure] = 'Absent'
|
184
|
-
result
|
185
|
-
end
|
186
|
-
|
187
191
|
# Attempts to set an instance of the DSC resource, invoking the `Set` method and thinly wrapping
|
188
192
|
# the `invoke_set_method` method; whether this method, `update`, or `delete` is called is entirely
|
189
193
|
# up to the Resource API based on the results
|
@@ -220,32 +224,40 @@ class Puppet::Provider::DscBaseProvider
|
|
220
224
|
invoke_set_method(context, name, name.merge({ dsc_ensure: 'Absent' }))
|
221
225
|
end
|
222
226
|
|
223
|
-
# Invokes the
|
224
|
-
# The PowerShell script returns a JSON
|
225
|
-
#
|
226
|
-
# fit the expected property definitions. Finally, it returns the object for the Resource API to
|
227
|
-
# 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.
|
228
230
|
#
|
229
231
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
230
232
|
# @param name_hash [Hash] the hash of namevars to be passed as properties to `Invoke-DscResource`
|
231
|
-
# @
|
232
|
-
|
233
|
-
|
234
|
-
|
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)
|
235
237
|
# Do not bother running if the logon credentials won't work
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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)
|
240
243
|
script_content = ps_script_content(resource)
|
241
244
|
context.debug("Script:\n #{redact_secrets(script_content)}")
|
242
|
-
output = ps_manager.execute(script_content)[:stdout]
|
243
|
-
|
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
|
244
250
|
|
245
|
-
|
251
|
+
begin
|
252
|
+
data = JSON.parse(output)
|
253
|
+
rescue => e
|
254
|
+
context.err(e)
|
255
|
+
return nil
|
256
|
+
end
|
246
257
|
context.debug("raw data received: #{data.inspect}")
|
258
|
+
|
247
259
|
error = data['errormessage']
|
248
|
-
unless error.nil?
|
260
|
+
unless error.nil? || error.empty?
|
249
261
|
# NB: We should have a way to stop processing this resource *now* without blowing up the whole Puppet run
|
250
262
|
# Raising an error stops processing but blows things up while context.err alerts but continues to process
|
251
263
|
if error =~ /Logon failure: the user has not been granted the requested logon type at this computer/
|
@@ -260,10 +272,47 @@ class Puppet::Provider::DscBaseProvider
|
|
260
272
|
# Either way, something went wrong and we didn't get back a good result, so return nil
|
261
273
|
return nil
|
262
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
|
+
|
263
312
|
# DSC gives back information we don't care about; filter down to only
|
264
313
|
# those properties exposed in the type definition.
|
265
314
|
valid_attributes = context.type.attributes.keys.collect(&:to_s)
|
266
|
-
parameters = context
|
315
|
+
parameters = parameter_attributes(context).collect(&:to_s)
|
267
316
|
data.select! { |key, _value| valid_attributes.include?("dsc_#{key.downcase}") }
|
268
317
|
data.reject! { |key, _value| parameters.include?("dsc_#{key.downcase}") }
|
269
318
|
# Canonicalize the results to match the type definition representation;
|
@@ -272,14 +321,26 @@ class Puppet::Provider::DscBaseProvider
|
|
272
321
|
data.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
273
322
|
type_key = "dsc_#{key.downcase}".to_sym
|
274
323
|
data[type_key] = data.delete(key)
|
275
|
-
|
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
|
+
|
276
331
|
# Convert DateTime back to appropriate type
|
277
|
-
|
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
|
278
341
|
# PowerShell does not distinguish between a return of empty array/string
|
279
342
|
# and null but Puppet does; revert to those values if specified.
|
280
|
-
if data[type_key].nil? && query_props.keys.include?(type_key) && query_props[type_key].is_a?(Array)
|
281
|
-
data[type_key] = query_props[type_key].empty? ? query_props[type_key] : []
|
282
|
-
end
|
343
|
+
data[type_key] = [] if data[type_key].nil? && query_props.keys.include?(type_key) && query_props[type_key].is_a?(Array)
|
283
344
|
end
|
284
345
|
# If a resource is found, it's present, so refill this Puppet-only key
|
285
346
|
data.merge!({ name: name_hash[:name] })
|
@@ -293,6 +354,9 @@ class Puppet::Provider::DscBaseProvider
|
|
293
354
|
# declaration is for an absent resource and the resource is actually absent
|
294
355
|
data.reject! { |_k, v| v.nil? } if data[:dsc_ensure] == 'Absent' && name_hash[:dsc_ensure] == 'Absent' && !name_hash_has_nil_keys
|
295
356
|
|
357
|
+
# Sort the return for order-insensitive nested enumerable comparison:
|
358
|
+
data = recursively_sort(data)
|
359
|
+
|
296
360
|
# Cache the query to prevent a second lookup
|
297
361
|
@@cached_query_results << data.dup if fetch_cached_hashes(@@cached_query_results, [data]).empty?
|
298
362
|
context.debug("Returned to Puppet as #{data}")
|
@@ -309,24 +373,36 @@ class Puppet::Provider::DscBaseProvider
|
|
309
373
|
def invoke_set_method(context, name, should)
|
310
374
|
context.debug("Invoking Set Method for '#{name}' with #{should.inspect}")
|
311
375
|
|
312
|
-
# Do not bother running if the logon credentials won't work
|
313
|
-
return nil if !should[:dsc_psdscrunascredential].nil? && logon_failed_already?(should[:dsc_psdscrunascredential])
|
314
|
-
|
315
376
|
apply_props = should.select { |k, _v| k.to_s =~ /^dsc_/ }
|
316
|
-
|
317
|
-
script_content = ps_script_content(resource)
|
318
|
-
context.debug("Script:\n #{redact_secrets(script_content)}")
|
377
|
+
invoke_dsc_resource(context, should, apply_props, 'set')
|
319
378
|
|
320
|
-
output = ps_manager.execute(script_content)[:stdout]
|
321
|
-
context.err('Nothing returned') if output.nil?
|
322
|
-
|
323
|
-
data = JSON.parse(output)
|
324
|
-
context.debug(data)
|
325
|
-
|
326
|
-
context.err(data['errormessage']) unless data['errormessage'].empty?
|
327
379
|
# TODO: Implement this functionality for notifying a DSC reboot?
|
328
380
|
# notify_reboot_pending if data['rebootrequired'] == true
|
329
|
-
|
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]
|
330
406
|
end
|
331
407
|
|
332
408
|
# Converts a Puppet resource hash into a hash with the information needed to call Invoke-DscResource,
|
@@ -337,10 +413,10 @@ class Puppet::Provider::DscBaseProvider
|
|
337
413
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
338
414
|
# @param dsc_invoke_method [String] the method to pass to Invoke-DscResource: get, set, or test
|
339
415
|
# @return [Hash] a hash with the information needed to run `Invoke-DscResource`
|
340
|
-
def
|
416
|
+
def invocable_resource(should, context, dsc_invoke_method)
|
341
417
|
resource = {}
|
342
418
|
resource[:parameters] = {}
|
343
|
-
%i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_module_name dscmeta_module_version].each do |k|
|
419
|
+
%i[name dscmeta_resource_friendly_name dscmeta_resource_name dscmeta_resource_implementation dscmeta_module_name dscmeta_module_version].each do |k|
|
344
420
|
resource[k] = context.type.definition[k]
|
345
421
|
end
|
346
422
|
should.each do |k, v|
|
@@ -356,6 +432,15 @@ class Puppet::Provider::DscBaseProvider
|
|
356
432
|
end
|
357
433
|
resource[:dsc_invoke_method] = dsc_invoke_method
|
358
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)
|
359
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;
|
360
445
|
# PROBLEM: This currently uses the downcased name, we need to capture the module name in the metadata I think.
|
361
446
|
# During a Puppet agent run, the code lives in the cache so we can use the file expansion to discover the correct folder.
|
@@ -363,36 +448,41 @@ class Puppet::Provider::DscBaseProvider
|
|
363
448
|
# path to allow multiple modules to with shared dsc_resources to be installed side by side
|
364
449
|
# The old vendored_modules_path: puppet_x/dsc_resources
|
365
450
|
# The new vendored_modules_path: puppet_x/<module_name>/dsc_resources
|
366
|
-
root_module_path =
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
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
|
372
457
|
|
373
458
|
# Check for the old vendored_modules_path second - if there is a mix of modules with the old and new pathing,
|
374
459
|
# checking for this first will always work and so the more specific search will never run.
|
375
|
-
unless File.exist?
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
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
|
381
466
|
end
|
382
467
|
|
383
468
|
# A warning is thrown if the something went wrong and the file was not created
|
384
|
-
raise "Unable to find expected vendored DSC Resource #{
|
469
|
+
raise "Unable to find expected vendored DSC Resource #{vendored_path}" unless File.exist? vendored_path
|
385
470
|
|
386
|
-
|
471
|
+
vendored_path
|
472
|
+
end
|
387
473
|
|
388
|
-
|
389
|
-
|
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
|
390
480
|
end
|
391
481
|
|
392
482
|
# Return a String containing a puppetized name. A puppetized name is a string that only
|
393
483
|
# includes lowercase letters, digits, underscores and cannot start with a digit.
|
394
484
|
#
|
395
|
-
# @return [String] with a
|
485
|
+
# @return [String] with a puppetized module name
|
396
486
|
def puppetize_name(name)
|
397
487
|
# Puppet module names must be lower case
|
398
488
|
name = name.downcase
|
@@ -435,20 +525,27 @@ class Puppet::Provider::DscBaseProvider
|
|
435
525
|
end
|
436
526
|
end
|
437
527
|
|
438
|
-
# Recursively transforms any enumerable,
|
528
|
+
# Recursively transforms any enumerable, downcasing any hash keys it finds, changing the passed enumerable.
|
439
529
|
#
|
440
530
|
# @param enumerable [Enumerable] a string, array, hash, or other object to attempt to recursively downcase
|
441
|
-
|
442
|
-
def camelcase_hash_keys!(enumerable)
|
531
|
+
def downcase_hash_keys!(enumerable)
|
443
532
|
if enumerable.is_a?(Hash)
|
444
533
|
enumerable.keys.each do |key| # rubocop:disable Style/HashEachMethods
|
445
|
-
name = key.dup
|
446
|
-
name[0] = name[0].downcase
|
534
|
+
name = key.dup.downcase
|
447
535
|
enumerable[name] = enumerable.delete(key)
|
448
|
-
|
536
|
+
downcase_hash_keys!(enumerable[name]) if enumerable[name].is_a?(Enumerable)
|
449
537
|
end
|
450
538
|
else
|
451
|
-
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) }
|
452
549
|
end
|
453
550
|
end
|
454
551
|
|
@@ -473,6 +570,34 @@ class Puppet::Provider::DscBaseProvider
|
|
473
570
|
end
|
474
571
|
end
|
475
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
|
+
|
476
601
|
# Parses the DSC resource type definition to retrieve the names of any attributes which are specified as mandatory for get operations
|
477
602
|
#
|
478
603
|
# @param context [Object] the Puppet runtime context to operate in and send feedback to
|
@@ -505,6 +630,16 @@ class Puppet::Provider::DscBaseProvider
|
|
505
630
|
context.type.attributes.select { |_name, properties| properties[:behaviour] == :parameter }.keys
|
506
631
|
end
|
507
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
|
+
|
508
643
|
# Look through a fully formatted string, replacing all instances where a value matches the formatted properties
|
509
644
|
# of an instantiated variable with references to the variable instead. This allows us to pass complex and nested
|
510
645
|
# CIM instances to the Invoke-DscResource parameter hash without constructing them *in* the hash.
|
@@ -520,7 +655,28 @@ class Puppet::Provider::DscBaseProvider
|
|
520
655
|
modified_string
|
521
656
|
end
|
522
657
|
|
523
|
-
# Parses a resource definition (as from `
|
658
|
+
# Parses a resource definition (as from `invocable_resource`) and, if the resource is implemented
|
659
|
+
# as a PowerShell class, ensures the System environment variable for PSModulePath is munged to
|
660
|
+
# include the vendored PowerShell modules. Due to a bug in PSDesiredStateConfiguration, class-based
|
661
|
+
# DSC Resources cannot be called via Invoke-DscResource by path, only by module name, *and* the
|
662
|
+
# module must be discoverable in the system-level PSModulePath. The postscript for invocation has
|
663
|
+
# logic to reset the system PSModulePath as stored in the script lines returned by this method.
|
664
|
+
#
|
665
|
+
# @param resource [Hash] a hash with the information needed to run `Invoke-DscResource`
|
666
|
+
# @return [String] A multi-line string which sets the PSModulePath at the system level
|
667
|
+
def munge_psmodulepath(resource)
|
668
|
+
return unless resource[:dscmeta_resource_implementation] == 'Class'
|
669
|
+
|
670
|
+
vendor_path = resource[:vendored_modules_path].gsub('/', '\\')
|
671
|
+
<<~MUNGE_PSMODULEPATH.strip
|
672
|
+
$UnmungedPSModulePath = [System.Environment]::GetEnvironmentVariable('PSModulePath','machine')
|
673
|
+
$MungedPSModulePath = $env:PSModulePath + ';#{vendor_path}'
|
674
|
+
[System.Environment]::SetEnvironmentVariable('PSModulePath', $MungedPSModulePath, [System.EnvironmentVariableTarget]::Machine)
|
675
|
+
$env:PSModulePath = [System.Environment]::GetEnvironmentVariable('PSModulePath','machine')
|
676
|
+
MUNGE_PSMODULEPATH
|
677
|
+
end
|
678
|
+
|
679
|
+
# Parses a resource definition (as from `invocable_resource`) for any properties which are PowerShell
|
524
680
|
# Credentials. As these values need to be serialized into PSCredential objects, return an array of
|
525
681
|
# PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
|
526
682
|
# These credential variables can then be simply assigned in the parameter hash where needed.
|
@@ -551,10 +707,10 @@ class Puppet::Provider::DscBaseProvider
|
|
551
707
|
# @param credential_hash [Hash] the Properties which define the PSCredential Object
|
552
708
|
# @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
|
553
709
|
def format_pscredential(variable_name, credential_hash)
|
554
|
-
"$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}'
|
710
|
+
"$#{variable_name} = New-PSCredential -User #{credential_hash['user']} -Password '#{credential_hash['password']}#{SECRET_POSTFIX}'"
|
555
711
|
end
|
556
712
|
|
557
|
-
# Parses a resource definition (as from `
|
713
|
+
# Parses a resource definition (as from `invocable_resource`) for any properties which are CIM instances
|
558
714
|
# whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
|
559
715
|
# those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
|
560
716
|
# will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
|
@@ -635,7 +791,7 @@ class Puppet::Provider::DscBaseProvider
|
|
635
791
|
interpolate_variables(definition)
|
636
792
|
end
|
637
793
|
|
638
|
-
# Munge a resource definition (as from `
|
794
|
+
# Munge a resource definition (as from `invocable_resource`) into valid PowerShell which represents
|
639
795
|
# the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
|
640
796
|
# defined variables into the hash.
|
641
797
|
#
|
@@ -649,7 +805,11 @@ class Puppet::Provider::DscBaseProvider
|
|
649
805
|
}
|
650
806
|
if resource.key?(:dscmeta_module_version)
|
651
807
|
params[:ModuleName] = {}
|
652
|
-
params[:ModuleName][:ModuleName] =
|
808
|
+
params[:ModuleName][:ModuleName] = if resource[:dscmeta_resource_implementation] == 'Class'
|
809
|
+
resource[:dscmeta_module_name]
|
810
|
+
else
|
811
|
+
"#{resource[:vendored_modules_path]}/#{resource[:dscmeta_module_name]}/#{resource[:dscmeta_module_name]}.psd1"
|
812
|
+
end
|
653
813
|
params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
|
654
814
|
else
|
655
815
|
params[:ModuleName] = resource[:dscmeta_module_name]
|
@@ -691,7 +851,7 @@ class Puppet::Provider::DscBaseProvider
|
|
691
851
|
params_block
|
692
852
|
end
|
693
853
|
|
694
|
-
# Given a resource definition (as from `
|
854
|
+
# Given a resource definition (as from `invocable_resource`), return a PowerShell script which has
|
695
855
|
# all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
|
696
856
|
# will correct munge the results for returning to Puppet as a JSON object.
|
697
857
|
#
|
@@ -706,13 +866,14 @@ class Puppet::Provider::DscBaseProvider
|
|
706
866
|
# The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
|
707
867
|
postscript = File.new("#{template_path}/invoke_dsc_resource_postscript.ps1").read
|
708
868
|
# The blocks define the variables to define for the postscript.
|
869
|
+
module_path_block = munge_psmodulepath(resource)
|
709
870
|
credential_block = prepare_credentials(resource)
|
710
871
|
cim_instances_block = prepare_cim_instances(resource)
|
711
872
|
parameters_block = invoke_params(resource)
|
712
873
|
# 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
|
713
874
|
clear_instantiated_variables!
|
714
875
|
|
715
|
-
[functions, preamble, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
|
876
|
+
[functions, preamble, module_path_block, credential_block, cim_instances_block, parameters_block, postscript].join("\n")
|
716
877
|
end
|
717
878
|
|
718
879
|
# Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
|
@@ -726,19 +887,18 @@ class Puppet::Provider::DscBaseProvider
|
|
726
887
|
rescue RuntimeError => e
|
727
888
|
raise unless e.message =~ /Sensitive \[value redacted\]/
|
728
889
|
|
729
|
-
|
730
|
-
string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
|
890
|
+
Pwsh::Util.format_powershell_value(unwrap(value))
|
731
891
|
end
|
732
892
|
|
733
|
-
# Unwrap sensitive strings for formatting, even inside an enumerable, appending
|
734
|
-
# to the end of the string in preparation for gsub cleanup.
|
893
|
+
# Unwrap sensitive strings for formatting, even inside an enumerable, appending the
|
894
|
+
# the secret postfix to the end of the string in preparation for gsub cleanup.
|
735
895
|
#
|
736
896
|
# @param value [Object] The object to unwrap sensitive data inside of
|
737
897
|
# @return [Object] The object with any sensitive strings unwrapped and annotated
|
738
898
|
def unwrap(value)
|
739
899
|
case value
|
740
900
|
when Puppet::Pops::Types::PSensitiveType::Sensitive
|
741
|
-
"#{value.unwrap}#
|
901
|
+
"#{value.unwrap}#{SECRET_POSTFIX}"
|
742
902
|
when Hash
|
743
903
|
unwrapped = {}
|
744
904
|
value.each do |k, v|
|
@@ -764,6 +924,47 @@ class Puppet::Provider::DscBaseProvider
|
|
764
924
|
text.gsub("'", "''")
|
765
925
|
end
|
766
926
|
|
927
|
+
# In order to avoid having to update the string that indicates when a value came from a sensitive
|
928
|
+
# string in multiple places, use a constant to indicate what the text of the secret identifier
|
929
|
+
# should be. This is used to write, identify, and redact secrets between PowerShell & Puppet.
|
930
|
+
SECRET_POSTFIX = '#PuppetSensitive'
|
931
|
+
|
932
|
+
# With multiple methods which need to discover secrets it is necessary to keep a single regex
|
933
|
+
# which can discover them. This will lazily match everything in a single-quoted string which
|
934
|
+
# ends with the secret postfix id and mark the actual contents of the string as the secret.
|
935
|
+
SECRET_DATA_REGEX = /'(?<secret>[^']+)+?#{Regexp.quote(SECRET_POSTFIX)}'/.freeze
|
936
|
+
|
937
|
+
# Strings containing sensitive data have a secrets postfix. These strings cannot be passed
|
938
|
+
# directly either to debug streams or to PowerShell and must be handled; this method contains
|
939
|
+
# the shared logic for parsing text for secrets and substituting values for them.
|
940
|
+
#
|
941
|
+
# @param text [String] the text to parse and handle for secrets
|
942
|
+
# @param replacement [String] the value to pass to gsub to replace secrets with
|
943
|
+
# @param error_message [String] the error message to raise instead of leaking secrets
|
944
|
+
# @return [String] the modified text
|
945
|
+
def handle_secrets(text, replacement, error_message)
|
946
|
+
# Every secret unwrapped in this module will unwrap as "'secret#{SECRET_POSTFIX}'"
|
947
|
+
# Currently, no known resources specify a SecureString instead of a PSCredential object.
|
948
|
+
return text unless text.match(/#{Regexp.quote(SECRET_POSTFIX)}/)
|
949
|
+
|
950
|
+
# In order to reduce time-to-parse, look at each line individually and *only* attempt
|
951
|
+
# to substitute if a naive match for the secret postfix is found on the line.
|
952
|
+
modified_text = text.split("\n").map do |line|
|
953
|
+
if line.match(/#{Regexp.quote(SECRET_POSTFIX)}/)
|
954
|
+
line.gsub(SECRET_DATA_REGEX, replacement)
|
955
|
+
else
|
956
|
+
line
|
957
|
+
end
|
958
|
+
end
|
959
|
+
|
960
|
+
modified_text = modified_text.join("\n")
|
961
|
+
|
962
|
+
# Something has gone wrong, error loudly
|
963
|
+
raise error_message if modified_text =~ /#{Regexp.quote(SECRET_POSTFIX)}/
|
964
|
+
|
965
|
+
modified_text
|
966
|
+
end
|
967
|
+
|
767
968
|
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
768
969
|
# and so for debugging purposes must be redacted before being sent to debug
|
769
970
|
# output but must *not* be redacted when sent to the PowerShell code manager.
|
@@ -771,15 +972,17 @@ class Puppet::Provider::DscBaseProvider
|
|
771
972
|
# @param text [String] the text to redact
|
772
973
|
# @return [String] the redacted text
|
773
974
|
def redact_secrets(text)
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
975
|
+
handle_secrets(text, "'#<Sensitive [value redacted]>'", "Unredacted sensitive data would've been leaked")
|
976
|
+
end
|
977
|
+
|
978
|
+
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
979
|
+
# and so the helper-id for sensitive data *must* be removed before sending to
|
980
|
+
# the PowerShell code manager.
|
981
|
+
#
|
982
|
+
# @param text [String] the text to strip of secret data identifiers
|
983
|
+
# @return [String] the modified text to pass to the PowerShell code manager
|
984
|
+
def remove_secret_identifiers(text)
|
985
|
+
handle_secrets(text, "'\\k<secret>'", 'Unable to properly format text for PowerShell with sensitive data')
|
783
986
|
end
|
784
987
|
|
785
988
|
# Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
|