ruby-pwsh 0.7.4 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +109 -0
- data/.gitignore +6 -1
- data/CHANGELOG.md +50 -3
- data/Gemfile +5 -1
- data/README.md +12 -0
- data/Rakefile +88 -0
- data/lib/puppet/provider/dsc_base_provider/dsc_base_provider.rb +334 -140
- 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/version.rb +1 -1
- data/lib/pwsh.rb +19 -45
- 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: bdcbcec4cceaa40f4fabaad50f9d7409382c14eb1a98a345af5770aa1ae78f31
|
4
|
+
data.tar.gz: 48f16a70a2305b398a9098d453ae743a6c2630a94cdc8d6716250e1e4ee83464
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c19486fd5452d81c71e7939fddaa78c66972ddb425806fc894b1aeecf0cb94b917955d29245130095ee5c4c85d63f2ba12176b6ef8fe4ce7e6f9f653bb0224c
|
7
|
+
data.tar.gz: b12c1ab52269745152a29ce446876a3e06f3b9f9aab1d254157d8f93c6cccb1e7f997c118488229e22d8800a1a1a17af0557a8c0af2ea386d3e80e80c2f5bfd0
|
@@ -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,7 +2,54 @@
|
|
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.10.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.10.1) (2021-08-23)
|
6
|
+
|
7
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.10.0...0.10.1)
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
|
11
|
+
- \(GH-180\) Ensure instance\_key respects full uniqueness of options [\#181](https://github.com/puppetlabs/ruby-pwsh/pull/181) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
12
|
+
- \(GH-165\) Ensure null-value nested cim instance arrays are appropriately munged [\#177](https://github.com/puppetlabs/ruby-pwsh/pull/177) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
13
|
+
|
14
|
+
## [0.10.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.10.0) (2021-07-02)
|
15
|
+
|
16
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.9.0...0.10.0)
|
17
|
+
|
18
|
+
### Added
|
19
|
+
|
20
|
+
- \(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))
|
21
|
+
|
22
|
+
## [0.9.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.9.0) (2021-06-28)
|
23
|
+
|
24
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.8.0...0.9.0)
|
25
|
+
|
26
|
+
### Added
|
27
|
+
|
28
|
+
- \(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))
|
29
|
+
- \(GH-145\) Improve DSC secrets redaction [\#150](https://github.com/puppetlabs/ruby-pwsh/pull/150) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
30
|
+
- \(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))
|
31
|
+
- \(MAINT\) Clarify supported platforms [\#113](https://github.com/puppetlabs/ruby-pwsh/pull/113) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
32
|
+
|
33
|
+
### Fixed
|
34
|
+
|
35
|
+
- \(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))
|
36
|
+
- \(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))
|
37
|
+
- \(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))
|
38
|
+
- \(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))
|
39
|
+
- \(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))
|
40
|
+
- \(GH-144\) Enable order-insensitive comparisons for DSC [\#151](https://github.com/puppetlabs/ruby-pwsh/pull/151) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
41
|
+
- \(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))
|
42
|
+
- \(GH-127\) Canonicalize enums correctly [\#131](https://github.com/puppetlabs/ruby-pwsh/pull/131) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
43
|
+
- \(GH-125\) Fix dsc provider canonicalization for absent resources [\#129](https://github.com/puppetlabs/ruby-pwsh/pull/129) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
44
|
+
- \(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))
|
45
|
+
- \(MODULES-11026\) Ensure the PowerShell manager works with v7 [\#122](https://github.com/puppetlabs/ruby-pwsh/pull/122) ([n3snah](https://github.com/n3snah))
|
46
|
+
- \(Maint\) Ensure canonicalize correctly compares sorted hashes [\#118](https://github.com/puppetlabs/ruby-pwsh/pull/118) ([Hvid](https://github.com/Hvid))
|
47
|
+
|
48
|
+
## [0.8.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.8.0) (2021-03-01)
|
49
|
+
|
50
|
+
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.4...0.8.0)
|
51
|
+
|
52
|
+
## [0.7.4](https://github.com/puppetlabs/ruby-pwsh/tree/0.7.4) (2021-02-12)
|
6
53
|
|
7
54
|
[Full Changelog](https://github.com/puppetlabs/ruby-pwsh/compare/0.7.3...0.7.4)
|
8
55
|
|
@@ -106,7 +153,6 @@ All notable changes to this project will be documented in this file.The format i
|
|
106
153
|
### Added
|
107
154
|
|
108
155
|
- \(IAC-1045\) Add the DSC base Puppet provider to pwshlib [\#39](https://github.com/puppetlabs/ruby-pwsh/pull/39) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
109
|
-
- \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
|
110
156
|
|
111
157
|
## [0.4.1](https://github.com/puppetlabs/ruby-pwsh/tree/0.4.1) (2020-02-13)
|
112
158
|
|
@@ -122,7 +168,7 @@ All notable changes to this project will be documented in this file.The format i
|
|
122
168
|
|
123
169
|
### Added
|
124
170
|
|
125
|
-
- \(
|
171
|
+
- \(MODULES-10389\) Add puppet feature for dependent modules to leverage [\#20](https://github.com/puppetlabs/ruby-pwsh/pull/20) ([sanfrancrisko](https://github.com/sanfrancrisko))
|
126
172
|
|
127
173
|
## [0.3.0](https://github.com/puppetlabs/ruby-pwsh/tree/0.3.0) (2019-12-04)
|
128
174
|
|
@@ -143,6 +189,7 @@ All notable changes to this project will be documented in this file.The format i
|
|
143
189
|
### Added
|
144
190
|
|
145
191
|
- \(FEAT\) Add quality of life utilities [\#11](https://github.com/puppetlabs/ruby-pwsh/pull/11) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
192
|
+
- \(FM-8422\) Make library releasable as a Puppet module [\#8](https://github.com/puppetlabs/ruby-pwsh/pull/8) ([michaeltlombardi](https://github.com/michaeltlombardi))
|
146
193
|
|
147
194
|
|
148
195
|
|
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,91 @@ 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
|
+
# Used in vendor_dsc_module
|
103
|
+
TAR_LONGLINK = '././@LongLink'
|
104
|
+
|
105
|
+
# Vendor a Puppetized DSC Module to spec/fixtures/modules.
|
106
|
+
#
|
107
|
+
# This is necessary because `puppet module install` fails on modules with
|
108
|
+
# long file paths, like xpsdesiredstateconfiguration
|
109
|
+
#
|
110
|
+
# @param command [String] command to execute.
|
111
|
+
# @return [Object] the standard out stream.
|
112
|
+
def vendor_dsc_module(name, version, destination)
|
113
|
+
require 'open-uri'
|
114
|
+
require 'rubygems/package'
|
115
|
+
require 'zlib'
|
116
|
+
|
117
|
+
module_uri = "https://forge.puppet.com/v3/files/dsc-#{name}-#{version}.tar.gz"
|
118
|
+
tar_gz_archive = File.expand_path("#{name}.tar.gz", ENV['TEMP'])
|
119
|
+
|
120
|
+
# Download the archive from the forge
|
121
|
+
File.open(tar_gz_archive, 'wb') do |file|
|
122
|
+
file.write(URI.open(module_uri).read) # rubocop:disable Security/Open
|
123
|
+
end
|
124
|
+
|
125
|
+
# Unzip to destination
|
126
|
+
# Taken directly from StackOverflow:
|
127
|
+
# - https://stackoverflow.com/a/19139114
|
128
|
+
Gem::Package::TarReader.new(Zlib::GzipReader.open(tar_gz_archive)) do |tar|
|
129
|
+
dest = nil
|
130
|
+
tar.each do |entry|
|
131
|
+
if entry.full_name == TAR_LONGLINK
|
132
|
+
dest = File.join(destination, entry.read.strip)
|
133
|
+
next
|
134
|
+
end
|
135
|
+
dest ||= File.join(destination, entry.full_name)
|
136
|
+
if entry.directory?
|
137
|
+
File.delete(dest) if File.file?(dest)
|
138
|
+
FileUtils.mkdir_p(dest, mode: entry.header.mode, verbose: false)
|
139
|
+
elsif entry.file?
|
140
|
+
FileUtils.rm_rf(dest) if File.directory?(dest)
|
141
|
+
File.open(dest, 'wb') do |f|
|
142
|
+
f.print(entry.read)
|
143
|
+
end
|
144
|
+
FileUtils.chmod(entry.header.mode, dest, verbose: false)
|
145
|
+
elsif entry.header.typeflag == '2' # Symlink!
|
146
|
+
File.symlink(entry.header.linkname, dest)
|
147
|
+
end
|
148
|
+
dest = nil
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Rename folder to just the module name, as needed by Puppet
|
153
|
+
Dir.glob("#{destination}/*#{name}*").each do |existing_folder|
|
154
|
+
new_folder = File.expand_path(name, destination)
|
155
|
+
FileUtils.mv(existing_folder, new_folder)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
namespace :dsc do
|
160
|
+
namespace :acceptance do
|
161
|
+
desc 'Prep for running DSC acceptance tests'
|
162
|
+
task :spec_prep do
|
163
|
+
# Create the modules fixture folder, if needed
|
164
|
+
modules_folder = File.expand_path('spec/fixtures/modules', File.dirname(__FILE__))
|
165
|
+
FileUtils.mkdir_p(modules_folder) unless Dir.exist?(modules_folder)
|
166
|
+
# symlink the parent folder to the modules folder for puppet
|
167
|
+
symlink_path = File.expand_path('pwshlib', modules_folder)
|
168
|
+
File.symlink(File.dirname(__FILE__), symlink_path) unless Dir.exist?(symlink_path)
|
169
|
+
# Install each of the required modules for acceptance testing
|
170
|
+
# Note: This only works for modules in the dsc namespace on the forge.
|
171
|
+
puppetized_dsc_modules = [
|
172
|
+
{ name: 'powershellget', version: '2.2.5-0-1' },
|
173
|
+
{ name: 'jeadsc', version: '0.7.2-0-2' }, # update to 0.7.2-0-3 on release
|
174
|
+
{ name: 'xpsdesiredstateconfiguration', version: '9.1.0-0-1' },
|
175
|
+
{ name: 'xwebadministration', version: '3.2.0-0-2' },
|
176
|
+
{ name: 'accesscontroldsc', version: '1.4.1-0-3' }
|
177
|
+
]
|
178
|
+
puppetized_dsc_modules.each do |puppet_module|
|
179
|
+
next if Dir.exist?(File.expand_path(puppet_module[:name], modules_folder))
|
180
|
+
|
181
|
+
vendor_dsc_module(puppet_module[:name], puppet_module[:version], modules_folder)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
185
|
+
t.pattern = 'spec/acceptance/dsc/*.rb'
|
186
|
+
end
|
187
|
+
end
|
188
|
+
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
70
92
|
end
|
71
|
-
#
|
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?
|
331
|
-
|
332
|
-
data = JSON.parse(output)
|
333
|
-
context.debug(data)
|
377
|
+
invoke_dsc_resource(context, should, apply_props, 'set')
|
334
378
|
|
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,10 +413,10 @@ 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
|
-
%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|
|
353
420
|
resource[k] = context.type.definition[k]
|
354
421
|
end
|
355
422
|
should.each do |k, v|
|
@@ -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,28 @@ 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`) 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
|
533
680
|
# Credentials. As these values need to be serialized into PSCredential objects, return an array of
|
534
681
|
# PowerShell lines, each of which instantiates a variable which holds the value as a PSCredential.
|
535
682
|
# These credential variables can then be simply assigned in the parameter hash where needed.
|
@@ -560,10 +707,10 @@ class Puppet::Provider::DscBaseProvider
|
|
560
707
|
# @param credential_hash [Hash] the Properties which define the PSCredential Object
|
561
708
|
# @return [String] A line of PowerShell which defines the PSCredential object and stores it to a variable
|
562
709
|
def format_pscredential(variable_name, credential_hash)
|
563
|
-
"$#{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}'"
|
564
711
|
end
|
565
712
|
|
566
|
-
# Parses a resource definition (as from `
|
713
|
+
# Parses a resource definition (as from `invocable_resource`) for any properties which are CIM instances
|
567
714
|
# whether at the top level or nested inside of other CIM instances, and, where they are discovered, adds
|
568
715
|
# those objects to the instantiated_variables hash as well as returning a line of PowerShell code which
|
569
716
|
# will create the CIM object and store it in a variable. This then allows the CIM instances to be assigned
|
@@ -644,7 +791,7 @@ class Puppet::Provider::DscBaseProvider
|
|
644
791
|
interpolate_variables(definition)
|
645
792
|
end
|
646
793
|
|
647
|
-
# Munge a resource definition (as from `
|
794
|
+
# Munge a resource definition (as from `invocable_resource`) into valid PowerShell which represents
|
648
795
|
# the `InvokeParams` hash which will be splatted to `Invoke-DscResource`, interpolating all previously
|
649
796
|
# defined variables into the hash.
|
650
797
|
#
|
@@ -658,7 +805,11 @@ class Puppet::Provider::DscBaseProvider
|
|
658
805
|
}
|
659
806
|
if resource.key?(:dscmeta_module_version)
|
660
807
|
params[:ModuleName] = {}
|
661
|
-
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
|
662
813
|
params[:ModuleName][:RequiredVersion] = resource[:dscmeta_module_version]
|
663
814
|
else
|
664
815
|
params[:ModuleName] = resource[:dscmeta_module_name]
|
@@ -700,7 +851,7 @@ class Puppet::Provider::DscBaseProvider
|
|
700
851
|
params_block
|
701
852
|
end
|
702
853
|
|
703
|
-
# Given a resource definition (as from `
|
854
|
+
# Given a resource definition (as from `invocable_resource`), return a PowerShell script which has
|
704
855
|
# all of the appropriate function and variable definitions, which will call Invoke-DscResource, and
|
705
856
|
# will correct munge the results for returning to Puppet as a JSON object.
|
706
857
|
#
|
@@ -715,13 +866,14 @@ class Puppet::Provider::DscBaseProvider
|
|
715
866
|
# The postscript defines the invocation error and result handling; expects `$InvokeParams` to be defined
|
716
867
|
postscript = File.new("#{template_path}/invoke_dsc_resource_postscript.ps1").read
|
717
868
|
# The blocks define the variables to define for the postscript.
|
869
|
+
module_path_block = munge_psmodulepath(resource)
|
718
870
|
credential_block = prepare_credentials(resource)
|
719
871
|
cim_instances_block = prepare_cim_instances(resource)
|
720
872
|
parameters_block = invoke_params(resource)
|
721
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
|
722
874
|
clear_instantiated_variables!
|
723
875
|
|
724
|
-
[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")
|
725
877
|
end
|
726
878
|
|
727
879
|
# Convert a Puppet/Ruby value into a PowerShell representation. Requires some slight additional
|
@@ -735,19 +887,18 @@ class Puppet::Provider::DscBaseProvider
|
|
735
887
|
rescue RuntimeError => e
|
736
888
|
raise unless e.message =~ /Sensitive \[value redacted\]/
|
737
889
|
|
738
|
-
|
739
|
-
string.gsub(/#PuppetSensitive'}/, "'} # PuppetSensitive")
|
890
|
+
Pwsh::Util.format_powershell_value(unwrap(value))
|
740
891
|
end
|
741
892
|
|
742
|
-
# Unwrap sensitive strings for formatting, even inside an enumerable, appending
|
743
|
-
# 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.
|
744
895
|
#
|
745
896
|
# @param value [Object] The object to unwrap sensitive data inside of
|
746
897
|
# @return [Object] The object with any sensitive strings unwrapped and annotated
|
747
898
|
def unwrap(value)
|
748
899
|
case value
|
749
900
|
when Puppet::Pops::Types::PSensitiveType::Sensitive
|
750
|
-
"#{value.unwrap}#
|
901
|
+
"#{value.unwrap}#{SECRET_POSTFIX}"
|
751
902
|
when Hash
|
752
903
|
unwrapped = {}
|
753
904
|
value.each do |k, v|
|
@@ -773,6 +924,47 @@ class Puppet::Provider::DscBaseProvider
|
|
773
924
|
text.gsub("'", "''")
|
774
925
|
end
|
775
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
|
+
|
776
968
|
# While Puppet is aware of Sensitive data types, the PowerShell script is not
|
777
969
|
# and so for debugging purposes must be redacted before being sent to debug
|
778
970
|
# output but must *not* be redacted when sent to the PowerShell code manager.
|
@@ -780,15 +972,17 @@ class Puppet::Provider::DscBaseProvider
|
|
780
972
|
# @param text [String] the text to redact
|
781
973
|
# @return [String] the redacted text
|
782
974
|
def redact_secrets(text)
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
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')
|
792
986
|
end
|
793
987
|
|
794
988
|
# Instantiate a PowerShell manager via the ruby-pwsh library and use it to invoke PowerShell.
|