ruby-pwsh 0.7.2 → 0.10.0
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 +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.
|