summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJay Mundrawala <jdmundrawala@gmail.com>2015-03-23 16:40:16 -0700
committerJay Mundrawala <jdmundrawala@gmail.com>2015-03-23 16:40:16 -0700
commitd0ee29bba624800fc1293d6144572aec42f602de (patch)
tree2c059eea11460c7335e30a6ceb9b338893ca22ac
parent815ad9cd0afb12cc7a9d81718d9a2179e78d9d61 (diff)
parentdeeb73825c5b6c7b3fc6738d4d6c484a8ba8307c (diff)
downloadchef-d0ee29bba624800fc1293d6144572aec42f602de.tar.gz
Merge pull request #2881 from chef/jdm/dsc_resource
DscResource in core chef
-rw-r--r--lib/chef/dsl/powershell.rb29
-rw-r--r--lib/chef/exceptions.rb15
-rw-r--r--lib/chef/mixin/powershell_type_coercions.rb82
-rw-r--r--lib/chef/mixin/windows_architecture_helper.rb8
-rw-r--r--lib/chef/platform/query_helpers.rb7
-rw-r--r--lib/chef/provider/dsc_resource.rb157
-rw-r--r--lib/chef/providers.rb1
-rw-r--r--lib/chef/recipe.rb2
-rw-r--r--lib/chef/resource/dsc_resource.rb83
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/util/dsc/resource_store.rb110
-rw-r--r--lib/chef/util/powershell/cmdlet.rb48
-rw-r--r--lib/chef/util/powershell/cmdlet_result.rb21
-rw-r--r--lib/chef/util/powershell/ps_credential.rb38
-rw-r--r--lib/chef/win32/api.rb2
-rw-r--r--lib/chef/win32/api/crypto.rb63
-rw-r--r--lib/chef/win32/crypto.rb49
-rw-r--r--spec/functional/resource/dsc_resource_spec.rb93
-rw-r--r--spec/functional/win32/crypto_spec.rb57
-rw-r--r--spec/unit/mixin/powershell_type_coercions_spec.rb72
-rw-r--r--spec/unit/platform/query_helpers_spec.rb22
-rw-r--r--spec/unit/provider/dsc_resource_spec.rb84
-rw-r--r--spec/unit/recipe_spec.rb4
-rw-r--r--spec/unit/resource/dsc_resource_spec.rb85
-rw-r--r--spec/unit/util/dsc/resource_store.rb76
-rw-r--r--spec/unit/util/powershell/ps_credential_spec.rb37
26 files changed, 1236 insertions, 10 deletions
diff --git a/lib/chef/dsl/powershell.rb b/lib/chef/dsl/powershell.rb
new file mode 100644
index 0000000000..a17971c689
--- /dev/null
+++ b/lib/chef/dsl/powershell.rb
@@ -0,0 +1,29 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/util/powershell/ps_credential'
+
+class Chef
+ module DSL
+ module Powershell
+ def ps_credential(username='placeholder', password)
+ Chef::Util::Powershell::PSCredential.new(username, password)
+ end
+ end
+ end
+end
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 22f090789f..eea6a2f239 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -442,5 +442,20 @@ class Chef
super "PID file and lockfile are not permitted to match. Specify a different location with --pid or --lockfile"
end
end
+
+ class MultipleDscResourcesFound < RuntimeError
+ attr_reader :resources_found
+ def initialize(resources_found)
+ @resources_found = resources_found
+ matches_info = @resources_found.each do |r|
+ if r['Module'].nil?
+ "Resource #{r['Name']} was found in #{r['Module']['Name']}"
+ else
+ "Resource #{r['Name']} is a binary resource"
+ end
+ end
+ super "Found multiple matching resources. #{matches_info.join("\n")}"
+ end
+ end
end
end
diff --git a/lib/chef/mixin/powershell_type_coercions.rb b/lib/chef/mixin/powershell_type_coercions.rb
new file mode 100644
index 0000000000..75b3276c84
--- /dev/null
+++ b/lib/chef/mixin/powershell_type_coercions.rb
@@ -0,0 +1,82 @@
+#
+# Author:: Adam Edwards (<adamed@opscode.com>)
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+ module Mixin
+ module PowershellTypeCoercions
+
+ def type_coercions
+ @type_coercions ||= {
+ Fixnum => { :type => lambda { |x| x.to_s }},
+ Float => { :type => lambda { |x| x.to_s }},
+ FalseClass => { :type => lambda { |x| '$false' }},
+ TrueClass => { :type => lambda { |x| '$true' }},
+ Hash => {:type => Proc.new { |x| translate_hash(x)}},
+ Array => {:type => Proc.new { |x| translate_array(x)}}
+ }
+ end
+
+ def translate_type(value)
+ translation = type_coercions[value.class]
+
+ if translation
+ translation[:type].call(value)
+ elsif value.respond_to? :to_psobject
+ "(#{value.to_psobject})"
+ else
+ safe_string(value.to_s)
+ end
+ end
+
+ private
+
+ def translate_hash(x)
+ translated = x.inject([]) do |memo, (k,v)|
+ memo << "#{k}=#{translate_type(v)}"
+ end
+ "@{#{translated.join(';')}}"
+ end
+
+ def translate_array(x)
+ translated = x.map do |v|
+ translate_type(v)
+ end
+ "@(#{translated.join(',')})"
+ end
+
+ def unsafe?(s)
+ ["'", '#', '`', '"'].any? do |x|
+ s.include? x
+ end
+ end
+
+ def safe_string(s)
+ # do we need to worry about binary data?
+ if unsafe?(s)
+ encoded_str = Base64.strict_encode64(s.encode("UTF-8"))
+ "([System.Text.Encoding]::UTF8.GetString("\
+ "[System.Convert]::FromBase64String('#{encoded_str}')"\
+ "))"
+ else
+ "'#{s}'"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb
index 65ad042910..a0ac34f627 100644
--- a/lib/chef/mixin/windows_architecture_helper.rb
+++ b/lib/chef/mixin/windows_architecture_helper.rb
@@ -43,6 +43,14 @@ class Chef
end
def with_os_architecture(node)
+ node ||= begin
+ os_arch = ENV['PROCESSOR_ARCHITEW6432'] ||
+ ENV['PROCESSOR_ARCHITECTURE']
+ Hash.new.tap do |n|
+ n[:kernel] = Hash.new
+ n[:kernel][:machine] = os_arch == 'AMD64' ? :x86_64 : :i386
+ end
+ end
wow64_redirection_state = nil
if wow64_architecture_override_required?(node, node_windows_architecture(node))
diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb
index ff83c871fa..f7c85fbe23 100644
--- a/lib/chef/platform/query_helpers.rb
+++ b/lib/chef/platform/query_helpers.rb
@@ -47,6 +47,13 @@ class Chef
node[:languages] && node[:languages][:powershell] &&
node[:languages][:powershell][:version].to_i >= 4
end
+
+ def supports_dsc_invoke_resource?(node)
+ require 'rubygems'
+ supports_dsc?(node) &&
+ Gem::Version.new(node[:languages][:powershell][:version]) >=
+ Gem::Version.new("5.0.10018.0")
+ end
end
end
end
diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb
new file mode 100644
index 0000000000..fabb695803
--- /dev/null
+++ b/lib/chef/provider/dsc_resource.rb
@@ -0,0 +1,157 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Chef Software, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/util/powershell/cmdlet'
+require 'chef/util/dsc/local_configuration_manager'
+require 'chef/mixin/powershell_type_coercions'
+require 'chef/util/dsc/resource_store'
+
+class Chef
+ class Provider
+ class DscResource < Chef::Provider
+ include Chef::Mixin::PowershellTypeCoercions
+
+ provides :dsc_resource, os: "windows"
+
+ def initialize(new_resource, run_context)
+ super
+ @new_resource = new_resource
+ @module_name = new_resource.module_name
+ end
+
+ def action_run
+ if ! test_resource
+ converge_by(generate_description) do
+ result = set_resource
+ end
+ end
+ end
+
+ def load_current_resource
+ end
+
+ def whyrun_supported?
+ true
+ end
+
+ def define_resource_requirements
+ requirements.assert(:run) do |a|
+ a.assertion { supports_dsc_invoke_resource? }
+ err = ["You must have Powershell version >= 5.0.10018.0 to use dsc_resource."]
+ a.failure_message Chef::Exceptions::NoProviderAvailable,
+ err
+ a.whyrun err + ["Assuming a previous resource installs Powershell 5.0.10018.0 or higher."]
+ a.block_action!
+ end
+ requirements.assert(:run) do |a|
+ a.assertion {
+ meta_configuration['RefreshMode'] == 'Disabled'
+ }
+ err = ["The LCM must have its RefreshMode set to Disabled. "]
+ a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ')
+ a.whyrun err + ["Assuming a previous resource sets the RefreshMode."]
+ a.block_action!
+ end
+ end
+
+ protected
+
+ def local_configuration_manager
+ @local_configuration_manager ||= Chef::Util::DSC::LocalConfigurationManager.new(
+ node,
+ nil
+ )
+ end
+
+ def resource_store
+ Chef::Util::DSC::ResourceStore.instance
+ end
+
+ def supports_dsc_invoke_resource?
+ run_context && Chef::Platform.supports_dsc_invoke_resource?(node)
+ end
+
+ def generate_description
+ @converge_description
+ end
+
+ def dsc_resource_name
+ new_resource.resource.to_s
+ end
+
+ def module_name
+ @module_name ||= begin
+ found = resource_store.find(dsc_resource_name)
+
+ r = case found.length
+ when 0
+ raise Chef::Exceptions::ResourceNotFound,
+ "Could not find #{dsc_resource_name}. Check to make "\
+ "sure that it shows up when running Get-DscResource"
+ when 1
+ if found[0]['Module'].nil?
+ :none
+ else
+ found[0]['Module']
+ end
+ else
+ raise Chef::Exceptions::MultipleDscResourcesFound, found
+ end
+ end
+ end
+
+ def test_resource
+ result = invoke_resource(:test)
+ # We really want this information from the verbose stream,
+ # however Invoke-DscResource is not correctly writing to that
+ # stream and instead just dumping to stdout
+ @converge_description = result.stdout
+ result.return_value[0]["InDesiredState"]
+ end
+
+ def set_resource
+ result = invoke_resource(:set)
+ result.return_value
+ end
+
+ def invoke_resource(method, output_format=:object)
+ properties = translate_type(@new_resource.properties)
+ switches = "-Method #{method.to_s} -Name #{@new_resource.resource}"\
+ " -Property #{properties} -Verbose"
+
+ if module_name != :none
+ switches += " -Module #{module_name}"
+ end
+
+ cmdlet = Chef::Util::Powershell::Cmdlet.new(
+ node,
+ "Invoke-DscResource #{switches}",
+ output_format
+ )
+ cmdlet.run!
+ end
+
+ def meta_configuration
+ cmdlet = Chef::Util::Powershell::Cmdlet.new(node, "Get-DscLocalConfigurationManager", :object)
+ result = cmdlet.run!
+ result.return_value
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index 796a0f8fa6..a5f5386de3 100644
--- a/lib/chef/providers.rb
+++ b/lib/chef/providers.rb
@@ -25,6 +25,7 @@ require 'chef/provider/cron/aix'
require 'chef/provider/deploy'
require 'chef/provider/directory'
require 'chef/provider/dsc_script'
+require 'chef/provider/dsc_resource'
require 'chef/provider/env'
require 'chef/provider/erl_call'
require 'chef/provider/execute'
diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb
index 91f7f30aa9..b4d37c2d61 100644
--- a/lib/chef/recipe.rb
+++ b/lib/chef/recipe.rb
@@ -25,6 +25,7 @@ require 'chef/dsl/include_recipe'
require 'chef/dsl/registry_helper'
require 'chef/dsl/reboot_pending'
require 'chef/dsl/audit'
+require 'chef/dsl/powershell'
require 'chef/mixin/from_file'
@@ -42,6 +43,7 @@ class Chef
include Chef::DSL::RegistryHelper
include Chef::DSL::RebootPending
include Chef::DSL::Audit
+ include Chef::DSL::Powershell
include Chef::Mixin::FromFile
include Chef::Mixin::Deprecation
diff --git a/lib/chef/resource/dsc_resource.rb b/lib/chef/resource/dsc_resource.rb
new file mode 100644
index 0000000000..912b683434
--- /dev/null
+++ b/lib/chef/resource/dsc_resource.rb
@@ -0,0 +1,83 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+#
+# Copyright:: 2014, Opscode, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+require 'chef/dsl/powershell'
+
+class Chef
+ class Resource
+ class DscResource < Chef::Resource
+
+ provides :dsc_resource, os: "windows"
+
+ include Chef::DSL::Powershell
+
+ def initialize(name, run_context)
+ super
+ @properties = {}
+ @resource_name = :dsc_resource
+ @resource = nil
+ @allowed_actions.push(:run)
+ @action = :run
+ end
+
+ def resource(value=nil)
+ if value
+ @resource = value
+ else
+ @resource
+ end
+ end
+
+ def module_name(value=nil)
+ if value
+ @module_name = value
+ else
+ @module_name
+ end
+ end
+
+ def property(property_name, value=nil)
+ if not property_name.is_a?(Symbol)
+ raise TypeError, "A property name of type Symbol must be specified, '#{property_name.to_s}' of type #{property_name.class.to_s} was given"
+ end
+
+ if value.nil?
+ value_of(@properties[property_name])
+ else
+ @properties[property_name] = value
+ end
+ end
+
+ def properties
+ @properties.reduce({}) do |memo, (k, v)|
+ memo[k] = value_of(v)
+ memo
+ end
+ end
+
+ private
+
+ def value_of(value)
+ if value.is_a?(DelayedEvaluator)
+ value.call
+ else
+ value
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index 680b393741..40b12a7c5f 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -29,6 +29,7 @@ require 'chef/resource/deploy_revision'
require 'chef/resource/directory'
require 'chef/resource/dpkg_package'
require 'chef/resource/dsc_script'
+require 'chef/resource/dsc_resource'
require 'chef/resource/easy_install_package'
require 'chef/resource/env'
require 'chef/resource/erl_call'
diff --git a/lib/chef/util/dsc/resource_store.rb b/lib/chef/util/dsc/resource_store.rb
new file mode 100644
index 0000000000..fdcecc2b3c
--- /dev/null
+++ b/lib/chef/util/dsc/resource_store.rb
@@ -0,0 +1,110 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+#
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/util/powershell/cmdlet'
+require 'chef/util/powershell/cmdlet_result'
+require 'chef/exceptions'
+
+class Chef
+class Util
+class DSC
+ class ResourceStore
+
+ def self.instance
+ @@instance ||= ResourceStore.new.tap do |store|
+ store.send(:populate_cache)
+ end
+ end
+
+ def resources
+ @resources ||= []
+ end
+
+ def find(name, module_name=nil)
+ found = find_resources(name, module_name, resources)
+
+ # We don't have it, query for the resource...it might
+ # have been added since we last queried
+ if found.length == 0
+ rs = query_resource(name)
+ add_resources(rs)
+ found = find_resources(name, module_name, rs)
+ end
+
+ found
+ end
+
+ private
+
+ def add_resource(new_r)
+ count = resources.count do |r|
+ r['ResourceType'].casecmp(new_r['ResourceType']) == 0
+ end
+ if count == 0
+ resources << new_r
+ end
+ end
+
+ def add_resources(rs)
+ rs.each do |r|
+ add_resource(r)
+ end
+ end
+
+ def populate_cache
+ @resources = query_resources
+ end
+
+ def find_resources(name, module_name, rs)
+ found = rs.find_all do |r|
+ name_matches = r['Name'].casecmp(name) == 0
+ if name_matches
+ module_name == nil || (r['Module'] and r['Module']['Name'].casecmp(module_name) == 0)
+ else
+ false
+ end
+ end
+ end
+
+
+ # Returns a list of dsc resources
+ def query_resources
+ cmdlet = Chef::Util::Powershell::Cmdlet.new(nil, 'get-dscresource',
+ :object)
+ result = cmdlet.run
+ result.return_value
+ end
+
+ # Returns a list of dsc resources matching the provided name
+ def query_resource(resource_name)
+ cmdlet = Chef::Util::Powershell::Cmdlet.new(nil, "get-dscresource #{resource_name}",
+ :object)
+ result = cmdlet.run
+ ret_val = result.return_value
+ if ret_val.nil?
+ []
+ elsif ret_val.is_a? Array
+ ret_val
+ else
+ [ret_val]
+ end
+ end
+ end
+end
+end
+end
diff --git a/lib/chef/util/powershell/cmdlet.rb b/lib/chef/util/powershell/cmdlet.rb
index 40edbb13c6..47d63a2b85 100644
--- a/lib/chef/util/powershell/cmdlet.rb
+++ b/lib/chef/util/powershell/cmdlet.rb
@@ -20,7 +20,9 @@ require 'mixlib/shellout'
require 'chef/mixin/windows_architecture_helper'
require 'chef/util/powershell/cmdlet_result'
-class Chef::Util::Powershell
+class Chef
+class Util
+class Powershell
class Cmdlet
def initialize(node, cmdlet, output_format=nil, output_format_options={})
@output_format = output_format
@@ -46,6 +48,10 @@ class Chef::Util::Powershell
attr_reader :output_format
def run(switches={}, execution_options={}, *arguments)
+ streams = { :json => CmdletStream.new('json'),
+ :verbose => CmdletStream.new('verbose'),
+ }
+
arguments_string = arguments.join(' ')
switches_string = command_switches_string(switches)
@@ -56,21 +62,25 @@ class Chef::Util::Powershell
json_depth = @output_format_options[:depth]
end
- json_command = @json_format ? " | convertto-json -compress -depth #{json_depth}" : ""
- command_string = "powershell.exe -executionpolicy bypass -noprofile -noninteractive -command \"trap [Exception] {write-error -exception ($_.Exception.Message);exit 1};#{@cmdlet} #{switches_string} #{arguments_string}#{json_command}\";if ( ! $? ) { exit 1 }"
+ json_command = @json_format ? " | convertto-json -compress -depth #{json_depth} "\
+ "> #{streams[:json].path}" : ""
+ redirections = "4> '#{streams[:verbose].path}'"
+ command_string = "powershell.exe -executionpolicy bypass -noprofile -noninteractive "\
+ "-command \"trap [Exception] {write-error -exception "\
+ "($_.Exception.Message);exit 1};#{@cmdlet} #{switches_string} "\
+ "#{arguments_string} #{redirections}"\
+ "#{json_command}\";if ( ! $? ) { exit 1 }"
augmented_options = {:returns => [0], :live_stream => false}.merge(execution_options)
command = Mixlib::ShellOut.new(command_string, augmented_options)
- os_architecture = "#{ENV['PROCESSOR_ARCHITEW6432']}" == 'AMD64' ? :x86_64 : :i386
-
status = nil
with_os_architecture(@node) do
status = command.run_command
end
- CmdletResult.new(status, @output_format)
+ CmdletResult.new(status, streams, @output_format)
end
def run!(switches={}, execution_options={}, *arguments)
@@ -131,6 +141,30 @@ class Chef::Util::Powershell
command_switches.join(' ')
end
+
+ class CmdletStream
+ def initialize(name)
+ @filename = Dir::Tmpname.create(name) {}
+ ObjectSpace.define_finalizer(self, self.class.destroy(@filename))
+ end
+
+ def path
+ @filename
+ end
+
+ def read
+ if File.exist? @filename
+ File.open(@filename, 'rb:bom|UTF-16LE') do |f|
+ f.read.encode('UTF-8')
+ end
+ end
+ end
+
+ def self.destroy(name)
+ proc { File.delete(name) if File.exists? name }
+ end
+ end
end
end
-
+end
+end
diff --git a/lib/chef/util/powershell/cmdlet_result.rb b/lib/chef/util/powershell/cmdlet_result.rb
index 246701a7bc..f1fdd968b1 100644
--- a/lib/chef/util/powershell/cmdlet_result.rb
+++ b/lib/chef/util/powershell/cmdlet_result.rb
@@ -18,22 +18,35 @@
require 'chef/json_compat'
-class Chef::Util::Powershell
+class Chef
+class Util
+class Powershell
class CmdletResult
attr_reader :output_format
- def initialize(status, output_format)
+ def initialize(status, streams, output_format)
@status = status
@output_format = output_format
+ @streams = streams
end
+ def stdout
+ @status.stdout
+ end
+
def stderr
@status.stderr
end
+ def stream(name)
+ @streams[name].read
+ end
+
def return_value
if output_format == :object
- Chef::JSONCompat.parse(@status.stdout)
+ Chef::JSONCompat.parse(stream(:json))
+ elsif output_format == :json
+ stream(:json)
else
@status.stdout
end
@@ -44,3 +57,5 @@ class Chef::Util::Powershell
end
end
end
+end
+end
diff --git a/lib/chef/util/powershell/ps_credential.rb b/lib/chef/util/powershell/ps_credential.rb
new file mode 100644
index 0000000000..01f8c27b6c
--- /dev/null
+++ b/lib/chef/util/powershell/ps_credential.rb
@@ -0,0 +1,38 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+#
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/crypto' if Chef::Platform.windows?
+
+class Chef::Util::Powershell
+ class PSCredential
+ def initialize(username, password)
+ @username = username
+ @password = password
+ end
+
+ def to_psobject
+ "New-Object System.Management.Automation.PSCredential('#{@username}',('#{encrypt(@password)}' | ConvertTo-SecureString))"
+ end
+
+ private
+
+ def encrypt(str)
+ Chef::ReservedNames::Win32::Crypto.encrypt(str)
+ end
+ end
+end
diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb
index 8b81947c9b..efa632f454 100644
--- a/lib/chef/win32/api.rb
+++ b/lib/chef/win32/api.rb
@@ -184,6 +184,8 @@ class Chef
host.typedef :pointer, :PSTR # Pointer to a null-terminated string of 8-bit Windows (ANSI) characters. For more information, see Character Sets Used By Fonts.
host.typedef :pointer, :PTBYTE # Pointer to a TBYTE.
host.typedef :pointer, :PTCHAR # Pointer to a TCHAR.
+ host.typedef :pointer, :PCRYPTPROTECT_PROMPTSTRUCT # Pointer to a CRYPTOPROTECT_PROMPTSTRUCT.
+ host.typedef :pointer, :PDATA_BLOB # Pointer to a DATA_BLOB.
host.typedef :pointer, :PTSTR # A PWSTR if UNICODE is defined, a PSTR otherwise.
host.typedef :pointer, :PUCHAR # Pointer to a UCHAR.
host.typedef :pointer, :PUHALF_PTR # Pointer to a UHALF_PTR.
diff --git a/lib/chef/win32/api/crypto.rb b/lib/chef/win32/api/crypto.rb
new file mode 100644
index 0000000000..1837a57557
--- /dev/null
+++ b/lib/chef/win32/api/crypto.rb
@@ -0,0 +1,63 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/api'
+
+class Chef
+ module ReservedNames::Win32
+ module API
+ module Crypto
+ extend Chef::ReservedNames::Win32::API
+
+ ###############################################
+ # Win32 API Bindings
+ ###############################################
+
+ ffi_lib 'Crypt32'
+
+ CRYPTPROTECT_UI_FORBIDDEN = 0x1
+ CRYPTPROTECT_LOCAL_MACHINE = 0x4
+ CRYPTPROTECT_AUDIT = 0x10
+
+ class CRYPT_INTEGER_BLOB < FFI::Struct
+ layout :cbData, :DWORD, # Count, in bytes, of data
+ :pbData, :pointer # Pointer to data buffer
+ def initialize(str=nil)
+ super(nil)
+ if str
+ self[:pbData] = FFI::MemoryPointer.from_string(str)
+ self[:cbData] = str.bytesize
+ end
+ end
+
+ end
+
+ safe_attach_function :CryptProtectData, [
+ :PDATA_BLOB,
+ :LPCWSTR,
+ :PDATA_BLOB,
+ :pointer,
+ :PCRYPTPROTECT_PROMPTSTRUCT,
+ :DWORD,
+ :PDATA_BLOB
+ ], :BOOL
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/win32/crypto.rb b/lib/chef/win32/crypto.rb
new file mode 100644
index 0000000000..79cf51b002
--- /dev/null
+++ b/lib/chef/win32/crypto.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/win32/error'
+require 'chef/win32/api/memory'
+require 'chef/win32/api/crypto'
+require 'digest'
+
+class Chef
+ module ReservedNames::Win32
+ class Crypto
+ include Chef::ReservedNames::Win32::API::Crypto
+ extend Chef::ReservedNames::Win32::API::Crypto
+
+ def self.encrypt(str, &block)
+ data_blob = CRYPT_INTEGER_BLOB.new
+ unless CryptProtectData(CRYPT_INTEGER_BLOB.new(str.to_wstring), nil, nil, nil, nil, 0, data_blob)
+ Chef::ReservedNames::Win32::Error.raise!
+ end
+ bytes = data_blob[:pbData].get_bytes(0, data_blob[:cbData])
+ if block
+ block.call(bytes)
+ else
+ Digest.hexencode(bytes)
+ end
+ ensure
+ unless data_blob[:pbData].null?
+ Chef::ReservedNames::Win32::Memory.local_free(data_blob[:pbData])
+ end
+ end
+
+ end
+ end
+end
diff --git a/spec/functional/resource/dsc_resource_spec.rb b/spec/functional/resource/dsc_resource_spec.rb
new file mode 100644
index 0000000000..6f453eeb9f
--- /dev/null
+++ b/spec/functional/resource/dsc_resource_spec.rb
@@ -0,0 +1,93 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'spec_helper'
+
+describe Chef::Resource::DscResource, :windows_powershell_dsc_only do
+ before(:all) do
+ @ohai = Ohai::System.new
+ @ohai.all_plugins(['platform', 'os', 'languages/powershell'])
+ end
+
+ let(:event_dispatch) { Chef::EventDispatch::Dispatcher.new }
+
+ let(:node) {
+ Chef::Node.new.tap do |n|
+ n.consume_external_attrs(@ohai.data, {})
+ end
+ }
+
+ let(:run_context) { Chef::RunContext.new(node, {}, event_dispatch) }
+
+ let(:new_resource) {
+ Chef::Resource::DscResource.new("dsc_resource_test", run_context)
+ }
+
+ context 'when Powershell does not support Invoke-DscResource'
+ context 'when Powershell supports Invoke-DscResource' do
+ before do
+ if !Chef::Platform.supports_dsc_invoke_resource?(node)
+ skip 'Requires Powershell >= 5.0.10018.0'
+ end
+ end
+ context 'with an invalid dsc resource' do
+ it 'raises an exception if the resource is not found' do
+ new_resource.resource 'thisdoesnotexist'
+ expect { new_resource.run_action(:run) }.to raise_error(
+ Chef::Exceptions::ResourceNotFound)
+ end
+ end
+
+ context 'with a valid dsc resource' do
+ let(:tmp_file_name) { Dir::Tmpname.create('tmpfile') {} }
+ let(:test_text) { "'\"!@#$%^&*)(}{][\u2713~n"}
+
+ before do
+ new_resource.resource :File
+ new_resource.property :Contents, test_text
+ new_resource.property :DestinationPath, tmp_file_name
+ end
+
+ after do
+ File.delete(tmp_file_name) if File.exists? tmp_file_name
+ end
+
+ it 'converges the resource if it is not converged' do
+ new_resource.run_action(:run)
+ contents = File.open(tmp_file_name, 'rb:bom|UTF-16LE') do |f|
+ f.read.encode('UTF-8')
+ end
+ expect(contents).to eq(test_text)
+ expect(new_resource).to be_updated
+ end
+
+ it 'does not converge the resource if it is already converged' do
+ new_resource.run_action(:run)
+ expect(new_resource).to be_updated
+ reresource =
+ Chef::Resource::DscResource.new("dsc_resource_retest", run_context)
+ reresource.resource :File
+ reresource.property :Contents, test_text
+ reresource.property :DestinationPath, tmp_file_name
+ reresource.run_action(:run)
+ expect(reresource).not_to be_updated
+ end
+ end
+
+ end
+end
diff --git a/spec/functional/win32/crypto_spec.rb b/spec/functional/win32/crypto_spec.rb
new file mode 100644
index 0000000000..1492995886
--- /dev/null
+++ b/spec/functional/win32/crypto_spec.rb
@@ -0,0 +1,57 @@
+#
+# Author:: Jay Mundrawala(<jdm@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'spec_helper'
+if Chef::Platform.windows?
+ require 'chef/win32/crypto'
+end
+
+describe 'Chef::ReservedNames::Win32::Crypto', :windows_only do
+ describe '#encrypt' do
+ before(:all) do
+ ohai_reader = Ohai::System.new
+ ohai_reader.all_plugins("platform")
+
+ new_node = Chef::Node.new
+ new_node.consume_external_attrs(ohai_reader.data,{})
+
+ events = Chef::EventDispatch::Dispatcher.new
+
+ @run_context = Chef::RunContext.new(new_node, {}, events)
+ end
+
+ let (:plaintext) { 'p@assword' }
+
+ it 'can be decrypted by powershell' do
+ encrypted = Chef::ReservedNames::Win32::Crypto.encrypt(plaintext)
+ resource = Chef::Resource::WindowsScript::PowershellScript.new("Powershell resource functional test", @run_context)
+ resource.code <<-EOF
+$encrypted = '#{encrypted}' | ConvertTo-SecureString
+$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($encrypted)
+$plaintext = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
+if ($plaintext -ne '#{plaintext}') {
+ Write-Error 'Got: ' $plaintext
+ exit 1
+}
+exit 0
+ EOF
+ resource.returns(0)
+ resource.run_action(:run)
+ end
+ end
+end
diff --git a/spec/unit/mixin/powershell_type_coercions_spec.rb b/spec/unit/mixin/powershell_type_coercions_spec.rb
new file mode 100644
index 0000000000..988c3926c1
--- /dev/null
+++ b/spec/unit/mixin/powershell_type_coercions_spec.rb
@@ -0,0 +1,72 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'spec_helper'
+require 'chef/mixin/powershell_type_coercions'
+require 'base64'
+
+class Chef::PSTypeTester
+ include Chef::Mixin::PowershellTypeCoercions
+end
+
+describe Chef::Mixin::PowershellTypeCoercions do
+ let (:test_class) { Chef::PSTypeTester.new }
+
+ describe '#translate_type' do
+ it 'should single quote a string' do
+ expect(test_class.translate_type('foo')).to eq("'foo'")
+ end
+
+ ["'", '"', '#', '`'].each do |c|
+ it "should base64 encode a string that contains #{c}" do
+ expect(test_class.translate_type("#{c}")).to match(Base64.strict_encode64(c))
+ end
+ end
+
+ it 'should not quote an integer' do
+ expect(test_class.translate_type(123)).to eq('123')
+ end
+
+ it 'should not quote a floating point number' do
+ expect(test_class.translate_type(123.4)).to eq('123.4')
+ end
+
+ it 'should return $false when an instance of FalseClass is provided' do
+ expect(test_class.translate_type(false)).to eq('$false')
+ end
+
+ it 'should return $true when an instance of TrueClass is provided' do
+ expect(test_class.translate_type(true)).to eq('$true')
+ end
+
+ it 'should translate all members of a hash and wrap them in @{} separated by ;' do
+ expect(test_class.translate_type({"a" => 1, "b" => 1.2, "c" => false, "d" => true
+ })).to eq("@{a=1;b=1.2;c=$false;d=$true}")
+ end
+
+ it 'should translat all members of an array and them by a ,' do
+ expect(test_class.translate_type([true, false])).to eq('@($true,$false)')
+ end
+
+ it 'should fall back :to_psobject if we have not defined at explicit rule' do
+ ps_obj = double("PSObject")
+ expect(ps_obj).to receive(:to_psobject).and_return('$true')
+ expect(test_class.translate_type(ps_obj)).to eq('($true)')
+ end
+ end
+end
diff --git a/spec/unit/platform/query_helpers_spec.rb b/spec/unit/platform/query_helpers_spec.rb
index 7aafc287ea..1dbd07a021 100644
--- a/spec/unit/platform/query_helpers_spec.rb
+++ b/spec/unit/platform/query_helpers_spec.rb
@@ -53,3 +53,25 @@ describe 'Chef::Platform#supports_dsc?' do
end
end
end
+
+describe 'Chef::Platform#supports_dsc_invoke_resource?' do
+ it 'returns false if powershell is not present' do
+ node = Chef::Node.new
+ expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_falsey
+ end
+
+ ['1.0', '2.0', '3.0', '4.0', '5.0.10017.9'].each do |version|
+ it "returns false for Powershell #{version}" do
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = version
+ expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_falsey
+ end
+ end
+
+ it "returns true for Powershell 5.0.10018.0" do
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = "5.0.10018.0"
+ expect(Chef::Platform.supports_dsc_invoke_resource?(node)).to be_truthy
+ end
+end
+
diff --git a/spec/unit/provider/dsc_resource_spec.rb b/spec/unit/provider/dsc_resource_spec.rb
new file mode 100644
index 0000000000..0a6c22bdcf
--- /dev/null
+++ b/spec/unit/provider/dsc_resource_spec.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Jay Mundrawala (<jdm@chef.io>)
+#
+# Copyright:: Copyright (c) 2014 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef'
+require 'spec_helper'
+
+describe Chef::Provider::DscResource do
+ let (:events) { Chef::EventDispatch::Dispatcher.new }
+ let (:run_context) { Chef::RunContext.new(node, {}, events) }
+ let (:resource) { Chef::Resource::DscResource.new("dscresource", run_context) }
+ let (:provider) do
+ Chef::Provider::DscResource.new(resource, run_context)
+ end
+
+ context 'when Powershell does not support Invoke-DscResource' do
+ let (:node) {
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = '4.0'
+ node
+ }
+
+ it 'raises a NoProviderAvailable exception' do
+ expect(provider).not_to receive(:meta_configuration)
+ expect{provider.run_action(:run)}.to raise_error(
+ Chef::Exceptions::NoProviderAvailable, /5\.0\.10018\.0/)
+ end
+ end
+
+ context 'when Powershell supports Invoke-DscResource' do
+ let (:node) {
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = '5.0.10018.0'
+ node
+ }
+
+ context 'when RefreshMode is not set to Disabled' do
+ let (:meta_configuration) { {'RefreshMode' => 'AnythingElse'}}
+
+ it 'raises an exception' do
+ expect(provider).to receive(:meta_configuration).and_return(
+ meta_configuration)
+ expect { provider.run_action(:run) }.to raise_error(
+ Chef::Exceptions::NoProviderAvailable, /Disabled/)
+ end
+ end
+
+ context 'when RefreshMode is set to Disabled' do
+ let (:meta_configuration) { {'RefreshMode' => 'Disabled'}}
+
+ it 'does not update the resource if it is up to date' do
+ expect(provider).to receive(:meta_configuration).and_return(
+ meta_configuration)
+ expect(provider).to receive(:test_resource).and_return(true)
+ provider.run_action(:run)
+ expect(resource).not_to be_updated
+ end
+
+ it 'converges the resource if it is not up to date' do
+ expect(provider).to receive(:meta_configuration).and_return(
+ meta_configuration)
+ expect(provider).to receive(:test_resource).and_return(false)
+ expect(provider).to receive(:set_resource)
+ provider.run_action(:run)
+ expect(resource).to be_updated
+ end
+ end
+ end
+end
diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb
index 8d0b1bcfd2..e1604483f3 100644
--- a/spec/unit/recipe_spec.rb
+++ b/spec/unit/recipe_spec.rb
@@ -593,5 +593,9 @@ describe Chef::Recipe do
expect(recipe.singleton_class.included_modules).to include(Chef::DSL::Audit)
expect(recipe.respond_to?(:control_group)).to be true
end
+
+ it "should respond to :ps_credential from Chef::DSL::Powershell" do
+ expect(recipe.respond_to?(:ps_credential)).to be true
+ end
end
end
diff --git a/spec/unit/resource/dsc_resource_spec.rb b/spec/unit/resource/dsc_resource_spec.rb
new file mode 100644
index 0000000000..ae15f56eaf
--- /dev/null
+++ b/spec/unit/resource/dsc_resource_spec.rb
@@ -0,0 +1,85 @@
+#
+# Author:: Adam Edwards (<adamed@getchef.com>)
+# Copyright:: Copyright (c) 2014 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'spec_helper'
+
+describe Chef::Resource::DscResource do
+ let(:dsc_test_resource_name) { 'DSCTest' }
+ let(:dsc_test_property_name) { :DSCTestProperty }
+ let(:dsc_test_property_value) { 'DSCTestValue' }
+
+ context 'when Powershell supports Dsc' do
+ let(:dsc_test_run_context) {
+ node = Chef::Node.new
+ node.automatic[:languages][:powershell][:version] = '5.0.10018.0'
+ empty_events = Chef::EventDispatch::Dispatcher.new
+ Chef::RunContext.new(node, {}, empty_events)
+ }
+ let(:dsc_test_resource) {
+ Chef::Resource::DscResource.new(dsc_test_resource_name, dsc_test_run_context)
+ }
+
+ it "has a default action of `:run`" do
+ expect(dsc_test_resource.action).to eq(:run)
+ end
+
+ it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do
+ expect(dsc_test_resource.allowed_actions.to_set).to eq([:run,:nothing].to_set)
+ end
+
+ it "allows the resource attribute to be set" do
+ dsc_test_resource.resource(dsc_test_resource_name)
+ expect(dsc_test_resource.resource).to eq(dsc_test_resource_name)
+ end
+
+ it "allows the module_name attribute to be set" do
+ dsc_test_resource.module_name(dsc_test_resource_name)
+ expect(dsc_test_resource.module_name).to eq(dsc_test_resource_name)
+ end
+
+ context "when setting a dsc property" do
+ it "allows setting a dsc property with a property name of type Symbol" do
+ dsc_test_resource.property(dsc_test_property_name, dsc_test_property_value)
+ expect(dsc_test_resource.property(dsc_test_property_name)).to eq(dsc_test_property_value)
+ expect(dsc_test_resource.properties[dsc_test_property_name]).to eq(dsc_test_property_value)
+ end
+
+ it "raises a TypeError if property_name is not a symbol" do
+ expect{
+ dsc_test_resource.property('Foo', dsc_test_property_value)
+ }.to raise_error(TypeError)
+ end
+
+ context "when using DelayedEvaluators" do
+ it "allows setting a dsc property with a property name of type Symbol" do
+ dsc_test_resource.property(dsc_test_property_name, Chef::DelayedEvaluator.new {
+ dsc_test_property_value
+ })
+ expect(dsc_test_resource.property(dsc_test_property_name)).to eq(dsc_test_property_value)
+ expect(dsc_test_resource.properties[dsc_test_property_name]).to eq(dsc_test_property_value)
+ end
+ end
+ end
+
+ context 'Powershell DSL methods' do
+ it "responds to :ps_credential" do
+ expect(dsc_test_resource.respond_to?(:ps_credential)).to be true
+ end
+ end
+ end
+end
diff --git a/spec/unit/util/dsc/resource_store.rb b/spec/unit/util/dsc/resource_store.rb
new file mode 100644
index 0000000000..a89e73fcaa
--- /dev/null
+++ b/spec/unit/util/dsc/resource_store.rb
@@ -0,0 +1,76 @@
+#
+# Author:: Jay Mundrawala <jdm@chef.io>
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef'
+require 'chef/util/dsc/resource_store'
+
+describe Chef::Util::DSC::ResourceStore do
+ let(:resource_store) { Chef::Util::DSC::ResourceStore.new }
+ let(:resource_a) { {
+ 'ResourceType' => 'AFoo',
+ 'Name' => 'Foo',
+ 'Module' => {'Name' => 'ModuleA'}
+ }
+ }
+
+ let(:resource_b) { {
+ 'ResourceType' => 'BFoo',
+ 'Name' => 'Foo',
+ 'Module' => {'Name' => 'ModuleB'}
+ }
+ }
+
+ context 'when resources are not cached' do
+ context 'when calling #resources' do
+ it 'returns an empty array' do
+ expect(resource_store.resources).to eql([])
+ end
+ end
+
+ context 'when calling #find' do
+ it 'returns an empty list if it cannot find any matching resources' do
+ expect(resource_store).to receive(:query_resource).and_return([])
+ expect(resource_store.find('foo')).to eql([])
+ end
+
+ it 'returns the resource if it is found (comparisons are case insensitive)' do
+ expect(resource_store).to receive(:query_resource).and_return([resource_a])
+ expect(resource_store.find('foo')).to eql([resource_a])
+ end
+
+ it 'returns multiple resoures if they are found' do
+ expect(resource_store).to receive(:query_resource).and_return([resource_a, resource_b])
+ expect(resource_store.find('foo')).to include(resource_a, resource_b)
+ end
+
+ it 'deduplicates resources by ResourceName' do
+ expect(resource_store).to receive(:query_resource).and_return([resource_a, resource_a])
+ resource_store.find('foo')
+ expect(resource_store.resources).to eq([resource_a])
+ end
+ end
+ end
+
+ context 'when resources are cached' do
+ it 'recalls resources from the cache if present' do
+ expect(resource_store).not_to receive(:query_resource)
+ expect(resource_store).to receive(:resources).and_return([resource_a])
+ resource_store.find('foo')
+ end
+ end
+end
diff --git a/spec/unit/util/powershell/ps_credential_spec.rb b/spec/unit/util/powershell/ps_credential_spec.rb
new file mode 100644
index 0000000000..bac58b02e5
--- /dev/null
+++ b/spec/unit/util/powershell/ps_credential_spec.rb
@@ -0,0 +1,37 @@
+#
+# Author:: Jay Mundrawala <jdm@chef.io>
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef'
+require 'chef/util/powershell/ps_credential'
+
+describe Chef::Util::Powershell::PSCredential do
+ let (:username) { 'foo' }
+ let (:password) { 'password' }
+
+ context 'when username and password are provided' do
+ let(:ps_credential) { Chef::Util::Powershell::PSCredential.new(username, password)}
+ context 'when calling to_psobject' do
+ it 'should create the script to create a PSCredential when calling' do
+ allow(ps_credential).to receive(:encrypt).with(password).and_return('encrypted')
+ expect(ps_credential.to_psobject).to eq(
+ "New-Object System.Management.Automation.PSCredential("\
+ "'#{username}',('encrypted' | ConvertTo-SecureString))")
+ end
+ end
+ end
+end