diff options
-rw-r--r-- | lib/chef/resource/powershell_package_source.rb | 165 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | spec/unit/resource/powershell_package_source_spec.rb | 219 |
3 files changed, 385 insertions, 0 deletions
diff --git a/lib/chef/resource/powershell_package_source.rb b/lib/chef/resource/powershell_package_source.rb new file mode 100644 index 0000000000..9fa4bc8497 --- /dev/null +++ b/lib/chef/resource/powershell_package_source.rb @@ -0,0 +1,165 @@ +# Author:: Tor Magnus Rakvåg (tm@intility.no) +# Copyright:: 2018, Intility AS +# 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/resource" +require "chef/json_compat" + +class Chef + class Resource + class PowershellPackageSource < Chef::Resource + preview_resource true + resource_name "powershell_package_source" + + description "Use the powershell_package_source resource to register a powershell package repository" + introduced "14.3" + + property :source_name, String, + description: "The name of the package source", + name_property: true + + property :url, String, + description: "The url to the package source", + required: true + + property :trusted, [TrueClass, FalseClass], + description: "Whether or not to trust packages from this source", + default: false + + property :provider_name, String, + equal_to: %w{ Programs msi NuGet msu PowerShellGet psl chocolatey }, + validation_message: "The following providers are supported: 'Programs', 'msi', 'NuGet', 'msu', 'PowerShellGet', 'psl' or 'chocolatey'", + description: "The package management provider for the source. It supports the following providers: 'Programs', 'msi', 'NuGet', 'msu', 'PowerShellGet', 'psl' and 'chocolatey'.", + default: "NuGet" + + property :publish_location, String, + description: "The url where modules will be published to for this source. Only valid if the provider is 'PowerShellGet'." + + property :script_source_location, String, + description: "The url where scripts are located for this source. Only valid if the provider is 'PowerShellGet'." + + property :script_publish_location, String, + description: "The location where scripts will be published to for this source. Only valid if the provider is 'PowerShellGet'." + + load_current_value do + cmd = load_resource_state_script(name) + repo = powershell_out!(cmd) + status = Chef::JSONCompat.from_json(repo.stdout) + url status["url"].nil? ? "not_set" : status["url"] + trusted status["trusted"] + provider_name status["provider_name"] + publish_location status["publish_location"] + script_source_location status["script_source_location"] + script_publish_location status["script_publish_location"] + end + + action :register do + description "Registers and updates the powershell package source." + # TODO: Ensure package provider is installed? + if psrepository_cmdlet_appropriate? + if package_source_exists? + converge_if_changed :url, :trusted, :publish_location, :script_source_location, :script_publish_location do + update_cmd = build_ps_repository_command("Set", new_resource) + res = powershell_out(update_cmd) + raise "Failed to update #{new_resource.source_name}: #{res.stderr}" unless res.stderr.empty? + end + else + converge_by("register source: #{new_resource.source_name}") do + register_cmd = build_ps_repository_command("Register", new_resource) + res = powershell_out(register_cmd) + raise "Failed to register #{new_resource.source_name}: #{res.stderr}" unless res.stderr.empty? + end + end + else + if package_source_exists? + converge_if_changed :url, :trusted, :provider_name do + update_cmd = build_package_source_command("Set", new_resource) + res = powershell_out(update_cmd) + raise "Failed to update #{new_resource.source_name}: #{res.stderr}" unless res.stderr.empty? + end + else + converge_by("register source: #{new_resource.source_name}") do + register_cmd = build_package_source_command("Register", new_resource) + res = powershell_out(register_cmd) + raise "Failed to register #{new_resource.source_name}: #{res.stderr}" unless res.stderr.empty? + end + end + end + end + + action :unregister do + description "Unregisters the powershell package source." + if package_source_exists? + unregister_cmd = "Get-PackageSource -Name '#{new_resource.source_name}' | Unregister-PackageSource" + converge_by("unregister source: #{new_resource.source_name}") do + res = powershell_out(unregister_cmd) + raise "Failed to unregister #{new_resource.source_name}: #{res.stderr}" unless res.stderr.empty? + end + end + end + + action_class do + def package_source_exists? + cmd = powershell_out!("(Get-PackageSource -Name '#{new_resource.source_name}').Name") + cmd.stdout.downcase.strip == new_resource.source_name.downcase + end + + def psrepository_cmdlet_appropriate? + new_resource.provider_name == "PowerShellGet" + end + + def build_ps_repository_command(cmdlet_type, new_resource) + cmd = "#{cmdlet_type}-PSRepository -Name '#{new_resource.source_name}'" + cmd << " -SourceLocation '#{new_resource.url}'" if new_resource.url + cmd << " -InstallationPolicy '#{new_resource.trusted ? "Trusted" : "Untrusted"}'" + cmd << " -PublishLocation '#{new_resource.publish_location}'" if new_resource.publish_location + cmd << " -ScriptSourceLocation '#{new_resource.script_source_location}'" if new_resource.script_source_location + cmd << " -ScriptPublishLocation '#{new_resource.script_publish_location}'" if new_resource.script_publish_location + cmd + end + + def build_package_source_command(cmdlet_type, new_resource) + cmd = "#{cmdlet_type}-PackageSource -Name '#{new_resource.source_name}'" + cmd << " -Location '#{new_resource.url}'" if new_resource.url + cmd << " -Trusted:#{new_resource.trusted ? "$true" : "$false"}" + cmd << " -ProviderName '#{new_resource.provider_name}'" if new_resource.provider_name + cmd + end + end + end + + private + + def load_resource_state_script(name) + <<-EOH + if(Get-PackageSource -Name '#{name}' -ErrorAction SilentlyContinue) { + if ((Get-PackageSource -Name '#{name}').ProviderName -eq 'PowerShellGet') { + (Get-PSRepository -Name '#{name}') | Select @{n='source_name';e={$_.Name}}, @{n='url';e={$_.SourceLocation}}, + @{n='trusted';e={$_.Trusted}}, @{n='provider_name';e={$_.PackageManagementProvider}}, @{n='publish_location';e={$_.PublishLocation}}, + @{n='script_source_location';e={$_.ScriptSourceLocation}}, @{n='script_publish_location';e={$_.ScriptPublishLocation}} | ConvertTo-Json + } + else { + (Get-PackageSource -Name '#{name}') | Select @{n='source_name';e={$_.Name}}, @{n='url';e={$_.Location}}, + @{n='provider_name';e={$_.ProviderName}}, @{n='trusted';e={$_.IsTrusted}} | ConvertTo-Json + } + } + else { + "" | Select source_name, url, provider_name, trusted | ConvertTo-Json + } + EOH + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index d942518b21..b21f2fe6f7 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -69,6 +69,7 @@ require "chef/resource/pacman_package" require "chef/resource/paludis_package" require "chef/resource/perl" require "chef/resource/portage_package" +require "chef/resource/powershell_package_source" require "chef/resource/powershell_script" require "chef/resource/osx_profile" require "chef/resource/python" diff --git a/spec/unit/resource/powershell_package_source_spec.rb b/spec/unit/resource/powershell_package_source_spec.rb new file mode 100644 index 0000000000..d8cb8a09a0 --- /dev/null +++ b/spec/unit/resource/powershell_package_source_spec.rb @@ -0,0 +1,219 @@ +# Author:: Tor Magnus Rakvåg (tm@intility.no) +# Copyright:: 2018, Intility AS +# 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::PowershellPackageSource do + let(:resource) { Chef::Resource::PowershellPackageSource.new("MyGallery") } + let(:provider) { resource.provider_for_action(:enable) } + + it "has a resource name of :powershell_package_source" do + expect(resource.resource_name).to eql(:powershell_package_source) + end + + it "the name_property is 'name'" do + expect(resource.source_name).to eql("MyGallery") + end + + it "the default action is :register" do + expect(resource.action).to eql([:register]) + end + + it "supports :register and :unregister actions" do + expect { resource.action :register }.not_to raise_error + expect { resource.action :unregister }.not_to raise_error + end + + it "the url property accepts strings" do + resource.url("https://mygallery.company.co/api/v2/") + expect(resource.url).to eql("https://mygallery.company.co/api/v2/") + end + + it "the trusted property accepts true and false" do + resource.trusted(false) + expect(resource.trusted).to eql(false) + resource.trusted(true) + expect(resource.trusted).to eql(true) + end + + it "trusted defaults to false" do + expect(resource.trusted).to eql(false) + end + + it "provider_name accepts 'Programs', 'msi', 'NuGet', 'msu', 'PowerShellGet', 'psl', 'chocolatey'" do + expect { resource.provider_name("Programs") }.not_to raise_error + expect { resource.provider_name("msi") }.not_to raise_error + expect { resource.provider_name("NuGet") }.not_to raise_error + expect { resource.provider_name("msu") }.not_to raise_error + expect { resource.provider_name("PowerShellGet") }.not_to raise_error + expect { resource.provider_name("psl") }.not_to raise_error + expect { resource.provider_name("chocolatey") }.not_to raise_error + end + + it "the publish_location property accepts strings" do + resource.publish_location("https://mygallery.company.co/api/v2/package") + expect(resource.publish_location).to eql("https://mygallery.company.co/api/v2/package") + end + + it "the script_source_location property accepts strings" do + resource.publish_location("https://mygallery.company.co/api/v2/scripts") + expect(resource.publish_location).to eql("https://mygallery.company.co/api/v2/scripts") + end + + it "the script_publish_location property accepts strings" do + resource.publish_location("https://mygallery.company.co/api/v2/scripts") + expect(resource.publish_location).to eql("https://mygallery.company.co/api/v2/scripts") + end + + describe "#build_ps_repository_command" do + before do + resource.source_name("MyGallery") + resource.url("https://mygallery.company.co/api/v2/") + end + + context "#register" do + it "builds a minimal command" do + expect(provider.build_ps_repository_command("Register", resource)).to eql("Register-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted'") + end + + it "builds a command with trusted set to true" do + resource.trusted(true) + expect(provider.build_ps_repository_command("Register", resource)).to eql("Register-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Trusted'") + end + + it "builds a command with a publish location" do + resource.publish_location("https://mygallery.company.co/api/v2/package") + expect(provider.build_ps_repository_command("Register", resource)).to eql("Register-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted' -PublishLocation 'https://mygallery.company.co/api/v2/package'") + end + + it "builds a command with a script source location" do + resource.script_source_location("https://mygallery.company.co/api/v2/scripts") + expect(provider.build_ps_repository_command("Register", resource)).to eql("Register-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted' -ScriptSourceLocation 'https://mygallery.company.co/api/v2/scripts'") + end + + it "builds a command with a script publish location" do + resource.script_publish_location("https://mygallery.company.co/api/v2/scripts/package") + expect(provider.build_ps_repository_command("Register", resource)).to eql("Register-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted' -ScriptPublishLocation 'https://mygallery.company.co/api/v2/scripts/package'") + end + end + + context "#set" do + it "builds a minimal command" do + expect(provider.build_ps_repository_command("Set", resource)).to eql("Set-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted'") + end + + it "builds a command to change the url" do + resource.url("https://othergallery.company.co/api/v2/") + expect(provider.build_ps_repository_command("Set", resource)).to eql("Set-PSRepository -Name 'MyGallery' -SourceLocation 'https://othergallery.company.co/api/v2/' -InstallationPolicy 'Untrusted'") + end + + it "builds a command with trusted set to true" do + resource.trusted(true) + expect(provider.build_ps_repository_command("Set", resource)).to eql("Set-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Trusted'") + end + + it "builds a command with a publish location" do + resource.publish_location("https://mygallery.company.co/api/v2/package") + expect(provider.build_ps_repository_command("Set", resource)).to eql("Set-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted' -PublishLocation 'https://mygallery.company.co/api/v2/package'") + end + + it "builds a command with a script source location" do + resource.script_source_location("https://mygallery.company.co/api/v2/scripts") + expect(provider.build_ps_repository_command("Set", resource)).to eql("Set-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted' -ScriptSourceLocation 'https://mygallery.company.co/api/v2/scripts'") + end + + it "builds a command with a script publish location" do + resource.script_publish_location("https://mygallery.company.co/api/v2/scripts/package") + expect(provider.build_ps_repository_command("Set", resource)).to eql("Set-PSRepository -Name 'MyGallery' -SourceLocation 'https://mygallery.company.co/api/v2/' -InstallationPolicy 'Untrusted' -ScriptPublishLocation 'https://mygallery.company.co/api/v2/scripts/package'") + end + end + end + + describe "#build_package_source_command" do + before do + resource.source_name("NuGet") + resource.url("http://nuget.org/api/v2/") + end + + context "#register" do + it "builds a minimal command" do + expect(provider.build_package_source_command("Register", resource)).to eql("Register-PackageSource -Name 'NuGet' -Location 'http://nuget.org/api/v2/' -Trusted:$false -ProviderName 'NuGet'") + end + + it "builds a command with trusted set to true" do + resource.trusted(true) + expect(provider.build_package_source_command("Register", resource)).to eql("Register-PackageSource -Name 'NuGet' -Location 'http://nuget.org/api/v2/' -Trusted:$true -ProviderName 'NuGet'") + end + + it "builds a command with a different provider" do + resource.source_name("choco") + resource.url("https://chocolatey.org/api/v2/") + resource.provider_name("chocolatey") + expect(provider.build_package_source_command("Register", resource)).to eql("Register-PackageSource -Name 'choco' -Location 'https://chocolatey.org/api/v2/' -Trusted:$false -ProviderName 'chocolatey'") + end + end + + context "#set" do + it "builds a minimal command" do + expect(provider.build_package_source_command("Set", resource)).to eql("Set-PackageSource -Name 'NuGet' -Location 'http://nuget.org/api/v2/' -Trusted:$false -ProviderName 'NuGet'") + end + + it "builds a command to change the url" do + resource.url("https://nuget.company.co/api/v2/") + expect(provider.build_package_source_command("Set", resource)).to eql("Set-PackageSource -Name 'NuGet' -Location 'https://nuget.company.co/api/v2/' -Trusted:$false -ProviderName 'NuGet'") + end + + it "builds a command with trusted set to true" do + resource.trusted(true) + expect(provider.build_package_source_command("Set", resource)).to eql("Set-PackageSource -Name 'NuGet' -Location 'http://nuget.org/api/v2/' -Trusted:$true -ProviderName 'NuGet'") + end + + it "builds a command with a different provider" do + resource.source_name("choco") + resource.url("https://chocolatey.org/api/v2/") + resource.provider_name("chocolatey") + expect(provider.build_package_source_command("Set", resource)).to eql("Set-PackageSource -Name 'choco' -Location 'https://chocolatey.org/api/v2/' -Trusted:$false -ProviderName 'chocolatey'") + end + end + end + + describe "#psrepository_cmdlet_appropriate?" do + it "returns true if the provider_name is 'PowerShellGet'" do + resource.provider_name("PowerShellGet") + expect(provider.psrepository_cmdlet_appropriate?).to eql(true) + end + + it "returns false if the provider_name is something else" do + resource.provider_name("NuGet") + expect(provider.psrepository_cmdlet_appropriate?).to eql(false) + end + end + + describe "#package_source_exists?" do + it "returns true if it exists" do + allow(provider).to receive(:powershell_out!).with("(Get-PackageSource -Name 'MyGallery').Name").and_return(double("powershell_out!", :stdout => "MyGallery\r\n")) + resource.source_name("MyGallery") + expect(provider.package_source_exists?).to eql(true) + end + + it "returns false if it doesn't exist" do + allow(provider).to receive(:powershell_out!).with("(Get-PackageSource -Name 'MyGallery').Name").and_return(double("powershell_out!", :stdout => "")) + resource.source_name("MyGallery") + expect(provider.package_source_exists?).to eql(false) + end + end +end |