diff --git a/lib/mixlib/install.rb b/lib/mixlib/install.rb index 048cb797..1e8934f8 100644 --- a/lib/mixlib/install.rb +++ b/lib/mixlib/install.rb @@ -26,6 +26,7 @@ require_relative "install/generator/bourne" require_relative "install/generator/powershell" require_relative "install/dist" +require_relative "install/product_matrix" module Mixlib class Install @@ -177,16 +178,15 @@ def download_artifact(directory = Dir.pwd) # Returns the base installation directory for the given options # # @return [String] the installation directory for the project + # habitat products use Habitat install directories # def root - # This only works for chef and chefdk but they are the only projects - # we are supporting as of now. - # chef-ice uses Habitat install directories - if options.product_name.casecmp("chef-ice") == 0 + product = PRODUCT_MATRIX.lookup(options.product_name) if options.product_name + if product&.habitat? if options.for_ps1? - "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\chef\\chef-infra-client\\*\\*" + "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\#{product.hab_origin}\\#{product.hab_package_name}\\*\\*" else - "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/chef/chef-infra-client/*/*" + "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/#{product.hab_origin}/#{product.hab_package_name}/*/*" end else if options.for_ps1? @@ -200,19 +200,15 @@ def root # # Returns the current version of the installed product. # Returns nil if the product is not installed. + # habitat products use Habitat install directories # def current_version - # Note that this logic does not work for products other than - # chef & chefdk since version-manifest is created under the - # install directory which can be different than the product name (e.g. - # chef-server -> /opt/opscode). But this is OK for now since - # chef & chefdk are the only supported products. - # chef-ice uses Habitat install directories - version_manifest_file = if options.product_name.casecmp("chef-ice") == 0 + product = PRODUCT_MATRIX.lookup(options.product_name) if options.product_name + version_manifest_file = if product&.habitat? if options.for_ps1? - "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\chef\\chef-infra-client\\*\\*\\version-manifest.json" + "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\#{product.hab_origin}\\#{product.hab_package_name}\\*\\*\\version-manifest.json" else - "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/chef/chef-infra-client/*/*/version-manifest.json" + "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/#{product.hab_origin}/#{product.hab_package_name}/*/*/version-manifest.json" end else if options.for_ps1? diff --git a/lib/mixlib/install/generator/base.rb b/lib/mixlib/install/generator/base.rb index 48d6a761..76fccf3d 100644 --- a/lib/mixlib/install/generator/base.rb +++ b/lib/mixlib/install/generator/base.rb @@ -19,6 +19,7 @@ require "ostruct" unless defined?(OpenStruct) require_relative "../util" require_relative "../dist" +require_relative "../product_matrix" module Mixlib class Install @@ -54,7 +55,16 @@ def self.get_script(name, context = {}) context[:support_url] ||= Mixlib::Install::Dist::SUPPORT_URL.freeze context[:resources_url] ||= Mixlib::Install::Dist::RESOURCES_URL.freeze context[:macos_dir] ||= Mixlib::Install::Dist::MACOS_VOLUME.freeze - context[:windows_dir] ||= context[:default_product].casecmp("chef-ice") == 0 ? Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR.freeze : Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR.freeze + context[:habitat_products] ||= begin + PRODUCT_MATRIX.products.each_with_object({}) do |pname, h| + p = PRODUCT_MATRIX.lookup(pname) + h[pname] = { origin: p.hab_origin, package_name: p.hab_package_name } if p.habitat? + end + end + context[:habitat_windows_dir] ||= + Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR.freeze + context[:omnibus_windows_dir] ||= + Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR.freeze context[:user_agent_string] = Util.user_agent_string(context[:user_agent_headers]) context_object = OpenStruct.new(context).instance_eval { binding } diff --git a/lib/mixlib/install/generator/powershell.rb b/lib/mixlib/install/generator/powershell.rb index 5db4d52c..6172d6cf 100644 --- a/lib/mixlib/install/generator/powershell.rb +++ b/lib/mixlib/install/generator/powershell.rb @@ -47,7 +47,7 @@ def install_command install_project_module = [] install_project_module << get_script("helpers.ps1", user_agent_headers: options.user_agent_headers) install_project_module << get_script("get_project_metadata.ps1", license_id: options.license_id, base_url: options.base_url) - install_project_module << get_script("install_project.ps1", license_id: options.license_id) + install_project_module << get_script("install_project.ps1", license_id: options.license_id, default_product: options.product_name) install_command = [] install_command << ps1_modularize(install_project_module.join("\n"), "Installer-Module") install_command << render_command diff --git a/lib/mixlib/install/generator/powershell/scripts/install_project.ps1.erb b/lib/mixlib/install/generator/powershell/scripts/install_project.ps1.erb index 739bcee7..6739a3a6 100644 --- a/lib/mixlib/install/generator/powershell/scripts/install_project.ps1.erb +++ b/lib/mixlib/install/generator/powershell/scripts/install_project.ps1.erb @@ -73,18 +73,23 @@ function Install-Project { $license_id = $env:CHEF_LICENSE_KEY } + # Habitat products mapped to their Habitat Builder origin/package names + $habitat_products = @{ +<% habitat_products.each do |pname, meta| %> + '<%= pname %>' = @{ origin = '<%= meta[:origin] %>'; package_name = '<%= meta[:package_name] %>' } +<% end %> + } + # Check for product installation in various locations - if ($project -eq 'chef' -or $project -eq 'chef-ice') { - # For chef or chef-ice, look for chef-infra-client paths + if ($habitat_products.ContainsKey($project)) { + $hab_meta = $habitat_products[$project] $install_locations = @( - "$env:systemdrive\hab\pkgs\chef\chef-infra-client\*\*\bin", - "$env:systemdrive\<%= windows_dir %>\chef\bin" + "$env:systemdrive\hab\pkgs\$($hab_meta.origin)\$($hab_meta.package_name)\*\*\bin", + "$env:systemdrive\<%= habitat_windows_dir %>\$($hab_meta.origin)\bin" ) } else { - # For other products, look for product-specific paths $install_locations = @( - "$env:systemdrive\hab\pkgs\chef\$project\*\*\bin", - "$env:systemdrive\<%= windows_dir %>\$project\bin" + "$env:systemdrive\<%= omnibus_windows_dir %>\$project\bin" ) } foreach ($path in $install_locations) { @@ -253,7 +258,8 @@ Function Install-ChefMsi($msi, $addlocal) { Function Install-ChefAppx($appx, $project) { Add-AppxPackage -Path $appx -ErrorAction Stop $package = (Get-AppxPackage -Name $project).InstallLocation - $installRoot = "$env:SystemDrive/<%= windows_dir %>" + # AppX packages are always omnibus-style installs, never habitat + $installRoot = "$env:SystemDrive/<%= omnibus_windows_dir %>" $omnibusRoot = Join-Path $installRoot $project if(!(Test-Path $installRoot)) { diff --git a/lib/mixlib/install/product.rb b/lib/mixlib/install/product.rb index a289caed..28552a83 100644 --- a/lib/mixlib/install/product.rb +++ b/lib/mixlib/install/product.rb @@ -37,6 +37,10 @@ def initialize(key, &block) :github_repo, :downloads_product_page_url, :api_url, + :distribution_type, + :hab_origin, + :hab_builder_url, + :hab_package_name ] # @@ -80,20 +84,42 @@ def initialize(key, &block) def default_value_for(prop) case prop when :install_path - "/opt/#{package_name}" + if distribution_type == "habitat" + "/hab/pkgs/#{hab_origin}/#{hab_package_name}" + else + "/opt/#{package_name}" + end when :omnibus_project package_name when :downloads_product_page_url - "#{Mixlib::Install::Dist::DOWNLOADS_PAGE}/#{product_key}" + "#{Mixlib::Install::Dist::DOWNLOADS_PAGE}" when :github_repo "#{Mixlib::Install::Dist::GITHUB_ORG}/#{product_key}" when :api_url ENV.fetch("PACKAGE_ROUTER_ENDPOINT", Mixlib::Install::Dist::PRODUCT_ENDPOINT) - else - nil + when :distribution_type + "omnibus" + when :hab_origin + "chef" + when :hab_builder_url + "https://bldr.habitat.sh" + when :hab_package_name + package_name end end + def habitat? + distribution_type == "habitat" + end + + def omnibus? + distribution_type == "omnibus" + end + + def hab_package_url + "#{hab_builder_url}/#/pkgs/#{hab_origin}/#{hab_package_name}/latest" + end + # # Return all known omnibus project names for a product # diff --git a/lib/mixlib/install/product_matrix.rb b/lib/mixlib/install/product_matrix.rb index cb142728..4bdfabbd 100644 --- a/lib/mixlib/install/product_matrix.rb +++ b/lib/mixlib/install/product_matrix.rb @@ -50,6 +50,8 @@ product "chef-ice" do product_name "Chef Infra Client Enterprise" package_name "chef-ice" + distribution_type "habitat" + hab_package_name "chef-infra-client" end product "chef-foundation" do @@ -111,7 +113,8 @@ product "chef-workstation-enterprise" do product_name "Chef Workstation Enterprise" package_name "chef-workstation-enterprise" - github_repo "chef/chef-workstation-enterprise" + distribution_type "habitat" + hab_package_name "chef-workstation" end product "chefdk" do @@ -164,6 +167,8 @@ product "inspec-enterprise" do product_name "Chef InSpec Enterprise" package_name "inspec-enterprise" + distribution_type "habitat" + hab_package_name "inspec" end product "mac-bootstrapper" do diff --git a/lib/mixlib/install/script_generator.rb b/lib/mixlib/install/script_generator.rb index 9ad98682..f8b67ae6 100644 --- a/lib/mixlib/install/script_generator.rb +++ b/lib/mixlib/install/script_generator.rb @@ -20,6 +20,7 @@ require_relative "util" require_relative "generator/powershell" require_relative "dist" +require_relative "product_matrix" require "cgi" module Mixlib @@ -99,14 +100,15 @@ def initialize(version, powershell = false, opts = {}) parse_opts(opts) - # Update root for chef-ice to use Habitat install directories - if @project&.casecmp("chef-ice") == 0 - @root = if powershell - "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\chef\\chef-infra-client\\*\\*" - else - "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/chef/chef-infra-client/*/*" - end - end + # Update root based on product distribution type (habitat vs omnibus) + product = PRODUCT_MATRIX.lookup(@project) if @project + return unless product&.habitat? + + @root = if powershell + "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\#{product.hab_origin}\\#{product.hab_package_name}\\*\\*" + else + "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/#{product.hab_origin}/#{product.hab_package_name}/*/*" + end end def install_command diff --git a/spec/functional/mixlib/install/cli_spec.rb b/spec/functional/mixlib/install/cli_spec.rb index 60acd6de..c65aa670 100644 --- a/spec/functional/mixlib/install/cli_spec.rb +++ b/spec/functional/mixlib/install/cli_spec.rb @@ -111,7 +111,7 @@ end end - context "with output option", :focus do + context "with output option" do let(:args) { "-o #{File.join(test_temp_dir, 'script.sh')}" } it "writes to a file" do diff --git a/spec/unit/mixlib/install/generator/base_spec.rb b/spec/unit/mixlib/install/generator/base_spec.rb index 2c434353..c9a08195 100644 --- a/spec/unit/mixlib/install/generator/base_spec.rb +++ b/spec/unit/mixlib/install/generator/base_spec.rb @@ -160,7 +160,7 @@ def self.script_base_path before do @temp_dir = Dir.mktmpdir @script_path = File.join(@temp_dir, "windows_dir.sh.erb") - File.write(@script_path, "dir=<%= windows_dir %>") + File.write(@script_path, "hab=<%= habitat_windows_dir %>\nomnibus=<%= omnibus_windows_dir %>") allow(test_generator_class).to receive(:script_base_path).and_return(@temp_dir) end @@ -169,18 +169,22 @@ def self.script_base_path FileUtils.rm_rf(@temp_dir) if @temp_dir end - it "uses habitat directory for chef-ice" do + it "sets habitat_products, habitat_windows_dir, omnibus_windows_dir" do context = { default_product: "chef-ice" } - script = test_generator_class.get_script("windows_dir.sh", context) + test_generator_class.get_script("windows_dir.sh", context) - expect(script).to include("dir=hab\\pkgs") + expect(context[:habitat_products]).to be_a(Hash) + expect(context[:habitat_products]["chef-ice"]).to eq({ origin: "chef", package_name: "chef-infra-client" }) + expect(context[:habitat_windows_dir]).to be_a(String) + expect(context[:omnibus_windows_dir]).to be_a(String) end - it "uses omnibus directory for chef" do - context = { default_product: "chef" } + it "renders habitat_windows_dir and omnibus_windows_dir in scripts" do + context = { default_product: "chef-ice" } script = test_generator_class.get_script("windows_dir.sh", context) - expect(script).to include("dir=opscode") + expect(script).to include("hab=hab\\pkgs") + expect(script).to include("omnibus=opscode") end end end @@ -236,7 +240,6 @@ def self.script_base_path support=<%= support_url %> resources=<%= resources_url %> macos=<%= macos_dir %> - windows=<%= windows_dir %> SCRIPT allow(test_generator_class).to receive(:script_base_path).and_return(@temp_dir) @@ -257,7 +260,6 @@ def self.script_base_path expect(script).to include("support=https://www.chef.io/support/tickets") expect(script).to include("resources=https://www.chef.io/support") expect(script).to include("macos=chef_software") - expect(script).to include("windows=opscode") end end end diff --git a/spec/unit/mixlib/install/generator_spec.rb b/spec/unit/mixlib/install/generator_spec.rb index e692a43b..52d71937 100644 --- a/spec/unit/mixlib/install/generator_spec.rb +++ b/spec/unit/mixlib/install/generator_spec.rb @@ -681,6 +681,77 @@ end end + context "habitat install-location hashtable for chef-ice" do + let(:add_options) do + { + product_name: "chef-ice", + shell_type: :ps1, + license_id: "test-license-key-123", + } + end + + it_behaves_like "the correct ps1 script" + + it "contains the habitat_products hashtable with chef-ice entry" do + expect(install_script).to include("'chef-ice' = @{ origin = 'chef'; package_name = 'chef-infra-client' }") + end + + it "uses runtime ContainsKey check not compile-time ERB branching" do + expect(install_script).to include("$habitat_products.ContainsKey($project)") + expect(install_script).not_to include("is_habitat") + end + + it "includes hab\\pkgs path in the habitat install location logic" do + expect(install_script).to include("hab\\pkgs\\$($hab_meta.origin)\\$($hab_meta.package_name)") + end + end + + context "habitat install-location hashtable for chef-workstation-enterprise" do + let(:add_options) do + { + product_name: "chef-workstation-enterprise", + shell_type: :ps1, + license_id: "test-license-key-123", + } + end + + it_behaves_like "the correct ps1 script" + + it "contains the habitat_products hashtable with chef-workstation-enterprise entry" do + expect(install_script).to include("'chef-workstation-enterprise' = @{ origin = 'chef'; package_name = 'chef-workstation' }") + end + + it "uses runtime ContainsKey check" do + expect(install_script).to include("$habitat_products.ContainsKey($project)") + end + end + + context "omnibus product (chef) does not use habitat paths" do + let(:add_options) do + { + product_name: "chef", + shell_type: :ps1, + license_id: "test-license-key-123", + } + end + + it_behaves_like "the correct ps1 script" + + it "still includes the habitat_products hashtable (runtime check)" do + expect(install_script).to include("$habitat_products = @{") + expect(install_script).to include("$habitat_products.ContainsKey($project)") + end + + it "does not hardcode habitat paths" do + expect(install_script).not_to include("\\hab\\pkgs\\chef\\chef-infra-client") + end + + it "includes omnibus install path via elseif branch" do + expect(install_script).to include("elseif ($project -eq 'chef')") + expect(install_script).to include("opscode\\chef\\bin") + end + end + context "optional package_manager parameter for PowerShell" do let(:add_options) do { diff --git a/spec/unit/mixlib/install/product_matrix_spec.rb b/spec/unit/mixlib/install/product_matrix_spec.rb index 1bc948c6..519712d6 100644 --- a/spec/unit/mixlib/install/product_matrix_spec.rb +++ b/spec/unit/mixlib/install/product_matrix_spec.rb @@ -256,4 +256,38 @@ end end end + + describe "habitat product distribution type" do + it "chef-ice has distribution_type habitat" do + expect(PRODUCT_MATRIX.lookup("chef-ice").distribution_type).to eq("habitat") + end + + it "chef-ice has hab_package_name chef-infra-client" do + expect(PRODUCT_MATRIX.lookup("chef-ice").hab_package_name).to eq("chef-infra-client") + end + + it "chef-workstation-enterprise has distribution_type habitat" do + expect(PRODUCT_MATRIX.lookup("chef-workstation-enterprise").distribution_type).to eq("habitat") + end + + it "chef-workstation-enterprise has hab_package_name chef-workstation" do + expect(PRODUCT_MATRIX.lookup("chef-workstation-enterprise").hab_package_name).to eq("chef-workstation") + end + + it "inspec-enterprise has distribution_type habitat" do + expect(PRODUCT_MATRIX.lookup("inspec-enterprise").distribution_type).to eq("habitat") + end + + it "inspec-enterprise has hab_package_name inspec" do + expect(PRODUCT_MATRIX.lookup("inspec-enterprise").hab_package_name).to eq("inspec") + end + + it "chef product has distribution_type omnibus" do + expect(PRODUCT_MATRIX.lookup("chef").distribution_type).to eq("omnibus") + end + + it "chef product omnibus? returns true" do + expect(PRODUCT_MATRIX.lookup("chef").omnibus?).to be true + end + end end diff --git a/spec/unit/mixlib/install/product_spec.rb b/spec/unit/mixlib/install/product_spec.rb index a3dc20ad..f99f5950 100644 --- a/spec/unit/mixlib/install/product_spec.rb +++ b/spec/unit/mixlib/install/product_spec.rb @@ -448,4 +448,71 @@ it_behaves_like "automate and delivery products" end + + context "habitat product support" do + let(:omnibus_product) do + Mixlib::Install::Product.new("chef") do + product_name "Chef Infra Client" + package_name "chef" + end + end + + let(:habitat_product) do + Mixlib::Install::Product.new("chef-ice") do + product_name "Chef Infra Client Enterprise" + package_name "chef-ice" + distribution_type "habitat" + hab_package_name "chef-infra-client" + end + end + + it "distribution_type defaults to omnibus" do + expect(omnibus_product.distribution_type).to eq("omnibus") + end + + it "hab_origin defaults to chef" do + expect(omnibus_product.hab_origin).to eq("chef") + end + + it "hab_builder_url defaults to https://bldr.habitat.sh" do + expect(omnibus_product.hab_builder_url).to eq("https://bldr.habitat.sh") + end + + it "hab_package_name defaults to package_name" do + expect(omnibus_product.hab_package_name).to eq("chef") + end + + it "habitat? returns false for omnibus product" do + expect(omnibus_product.habitat?).to be false + end + + it "habitat? returns true for habitat product" do + expect(habitat_product.habitat?).to be true + end + + it "omnibus? returns true for omnibus product" do + expect(omnibus_product.omnibus?).to be true + end + + it "omnibus? returns false for habitat product" do + expect(habitat_product.omnibus?).to be false + end + + it "install_path for omnibus product is /opt/" do + expect(omnibus_product.install_path).to eq("/opt/chef") + end + + it "install_path for habitat product is /hab/pkgs/chef/" do + expect(habitat_product.install_path).to eq("/hab/pkgs/chef/chef-infra-client") + end + + it "hab_package_url returns correct URL" do + expect(habitat_product.hab_package_url).to eq("https://bldr.habitat.sh/#/pkgs/chef/chef-infra-client/latest") + end + + it "hab_package_name explicit override works when product key differs from package name" do + expect(habitat_product.hab_package_name).to eq("chef-infra-client") + expect(habitat_product.package_name).to eq("chef-ice") + end + end end diff --git a/spec/unit/mixlib/install/script_generator_spec.rb b/spec/unit/mixlib/install/script_generator_spec.rb index d3389361..49c6e180 100644 --- a/spec/unit/mixlib/install/script_generator_spec.rb +++ b/spec/unit/mixlib/install/script_generator_spec.rb @@ -64,6 +64,23 @@ expect(install.root).to eq("/hab/pkgs/chef/chef-infra-client/*/*") end end + + describe "habitat product root uses metadata-driven paths" do + it "chef-ice unix root uses hab_origin and hab_package_name from product metadata" do + install = described_class.new("1.2.1", false, project: "chef-ice") + expect(install.root).to include("/hab/pkgs/chef/chef-infra-client") + end + + it "chef-ice windows root uses hab_origin and hab_package_name from product metadata" do + install = described_class.new("1.2.1", true, project: "chef-ice") + expect(install.root).to match(/\A\$env:systemdrive.*hab.pkgs.chef.chef-infra-client/i) + end + + it "non-habitat product uses omnibus path" do + install = described_class.new("1.2.1", false, project: "chef") + expect(install.root).to eq("/opt/chef") + end + end end describe "parses the options hash" do diff --git a/spec/unit/mixlib/install_spec.rb b/spec/unit/mixlib/install_spec.rb index 6247dffb..6e478c1c 100644 --- a/spec/unit/mixlib/install_spec.rb +++ b/spec/unit/mixlib/install_spec.rb @@ -97,6 +97,28 @@ end end end + + context "with chef-workstation-enterprise product" do + let(:product_name) { "chef-workstation-enterprise" } + let(:version_manifest_file) { "/hab/pkgs/chef/chef-workstation/*/*/version-manifest.json" } + + it "should use Habitat install directory path" do + expect(installer.root).to eq("/hab/pkgs/chef/chef-workstation/*/*") + end + + context "when chef-workstation-enterprise is installed" do + before do + expect(File).to receive(:exist?).with(version_manifest_file).and_return(true) + expect(File).to receive(:read).with(version_manifest_file).and_wrap_original do |m, path| + m.call(File.join(VERSION_MANIFEST_DIR, "/opt/chef/version-manifest.json")) + end + end + + it "should report version correctly" do + expect(installer.current_version).to eq("12.4.3") + end + end + end end context "checking for upgrades", :vcr do