From 39118d3c4aa2b81c905fb4f022b3f7373a3b2a6c Mon Sep 17 00:00:00 2001 From: Corey Hemminger Date: Fri, 13 Feb 2026 18:03:17 -0600 Subject: [PATCH] adding chef-ice package_manager parameter Signed-off-by: Corey Hemminger --- .github/copilot-instructions.md | 329 +++++++++++++++++- .github/workflows/integration.yml | 136 ++++++++ README.md | 104 ++++++ generate_install_scripts.sh | 177 ++++++++++ lib/mixlib/install.rb | 59 +++- lib/mixlib/install/backend/base.rb | 16 +- lib/mixlib/install/backend/package_router.rb | 77 +++- lib/mixlib/install/generator/base.rb | 11 +- lib/mixlib/install/generator/bourne.rb | 3 +- .../bourne/scripts/fetch_metadata.sh.erb | 104 +++++- .../generator/bourne/scripts/fetch_package.sh | 24 +- .../scripts/script_cli_parameters.sh.erb | 5 +- lib/mixlib/install/generator/powershell.rb | 3 +- .../scripts/get_project_metadata.ps1.erb | 71 ++-- .../scripts/install_project.ps1.erb | 44 ++- lib/mixlib/install/options.rb | 1 + lib/mixlib/install/product_matrix.rb | 2 +- lib/mixlib/install/script_generator.rb | 64 ++-- lib/mixlib/install/util.rb | 47 +++ .../install/backend/package_router_spec.rb | 180 ++++++++++ .../mixlib/install/generator/base_spec.rb | 28 +- spec/unit/mixlib/install/generator_spec.rb | 239 +++++++++++++ spec/unit/mixlib/install/options_spec.rb | 16 + .../mixlib/install/script_generator_spec.rb | 92 +++++ spec/unit/mixlib/install/util_spec.rb | 84 +++++ spec/unit/mixlib/install_spec.rb | 22 ++ support/install_command.ps1 | 1 + 27 files changed, 1805 insertions(+), 134 deletions(-) create mode 100755 generate_install_scripts.sh diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 17e731d3..5cbf336f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,6 +5,25 @@ Mixlib::Install is a library for interacting with Chef Software Inc's software d **Primary Goal**: Support the widest range of Ruby versions possible to ensure compatibility across diverse Chef environments. +**Recent Major Changes** (v3.13.0 - v3.15.0): +- **PR #417**: Added chef-ice product with package_manager parameter support + - Implemented platform normalization (`Util.normalize_platform_for_commercial`) for chef-ice + - Added package manager detection (`Util.determine_package_manager`) for automatic format selection + - Updated URL construction to use `m`, `p`, `pm` parameters for chef-ice (vs. `p`, `pv`, `m` for standard products) + - Modified shell and PowerShell scripts to handle chef-ice metadata URLs +- **PR #408, #416**: Added commercial and trial API support for licensed Chef products + - Implemented license_id parameter for install scripts and API calls + - Added trial API automatic defaults enforcement (stable channel, latest version only with warnings) + - Created `Dist.trial_license?` and `Dist.commercial_license?` helper methods +- **Install Directory Refactoring**: Support for both Omnibus and Habitat package paths + - Renamed `WINDOWS_INSTALL_DIR` → `OMNIBUS_WINDOWS_INSTALL_DIR`, `LINUX_INSTALL_DIR` → `OMNIBUS_LINUX_INSTALL_DIR` + - Added `HABITAT_WINDOWS_INSTALL_DIR = "hab\\pkgs"` and `HABITAT_LINUX_INSTALL_DIR = "/hab/pkgs"` + - Updated `root` and `current_version` methods in `lib/mixlib/install.rb` to conditionally use Habitat paths for chef-ice + - Modified script generators to set appropriate install directories based on product type +- **PR #413**: Added `list-products` CLI subcommand for product matrix discovery +- **PR #407**: Added Habitat package path detection to generated install scripts +- **PR #411**: Migrated CI from Buildkite to GitHub Actions with comprehensive test coverage + ## Ruby Version Support Strategy ### Supported Ruby Versions @@ -82,6 +101,9 @@ All Ruby files should include the Apache 2.0 license header: - Supports EXTRA_PRODUCTS_FILE environment variable for custom products - Key options: channel, product_name, product_version, platform, platform_version, architecture, license_id - **license_id**: Enables commercial/trial API access for licensed Chef products + - **Trial API Enforcement**: Automatically defaults channel to :stable and product_version to :latest when trial license detected + - Uses `enforce_trial_api_defaults!` method during initialization to apply restrictions + - Emits warnings to stderr when defaults are applied 1. **Product Matrix** (`lib/mixlib/install/product_matrix.rb`) - DSL for defining product metadata @@ -133,6 +155,18 @@ bundle exec rake # All tests (default) - To update cassettes, see instructions in `spec/spec_helper.rb` - Functional tests disable VCR to test live interactions +### Gemspec vs Gemfile Dependencies +**Gemspec** (`mixlib-install.gemspec`): +- Runtime dependencies only +- Minimal dependencies: mixlib-shellout, mixlib-versioning, thor +- No version constraints in latest version (dependencies have their own compatibility handling) + +**Gemfile**: +- Development and test dependencies +- Ruby version-specific constraints for test tools +- Includes chefstyle for linting (~> 0.4.0) +- VCR for HTTP mocking in tests + ### Ruby Version-Specific Test Dependencies The Gemfile contains careful version constraints for test dependencies based on RUBY_VERSION: - Ruby < 2.6: Specific version pins for chef-utils, climate_control, mixlib-shellout, vcr @@ -205,9 +239,23 @@ The gemspec includes special handling for the openssl gem due to CRL checking is ### Common Commands ```bash mixlib-install download chef # Download latest stable chef +mixlib-install list-products # List all available products (added in v3.14.0) mixlib-install help # Show all commands ``` +### Available Subcommands +- `download` - Download a Chef Software product +- `list-products` - Display all available products from the product matrix +- `list-versions` - List available versions for a product +- `help` - Display help information + +### Generated Script Parameters +When using `install_sh()` or `install_ps1()` methods or CLI-generated scripts: +- `-b ` / `-base_api_url ` (shell): Override API endpoint +- `-L ` / `-license_id ` (shell): Provide license ID for commercial/trial API +- `-l ` (PowerShell): Provide license ID for commercial/trial API +- Scripts automatically detect correct API endpoint based on license_id prefix if base URL not provided + ## Platform Version Compatibility Mode The library includes sophisticated platform version compatibility logic: @@ -221,6 +269,12 @@ The library includes sophisticated platform version compatibility logic: - Supports: http_proxy, https_proxy, ftp_proxy, no_proxy - Platform detection for Linux/Unix systems - Generated via `lib/mixlib/install/generator/bourne.rb` +- **API Endpoint Selection**: Uses `base_api_url` parameter to determine endpoint: + - If `base_api_url` is empty and `license_id` is provided: + - Trial API: `https://chefdownload-trial.chef.io` (for `free-*` or `trial-*` prefixes) + - Commercial API: `https://chefdownload-commercial.chef.io` (for other license IDs) + - If `base_api_url` is empty and no `license_id`: Omnitruck API `https://omnitruck.chef.io` + - If `base_api_url` is set: Uses the provided URL (allows override) - **Content-Disposition Support**: When `license_id` is provided: - Downloads to temp file: `chef-download-temp.$$` - Extracts filename from HTTP response headers (3 methods): @@ -230,22 +284,38 @@ The library includes sophisticated platform version compatibility logic: - Fallback: Constructs filename from platform metadata if extraction fails - Renames temp file to extracted/constructed filename - Works with all download methods: wget, curl, fetch, perl, python +- **Chef-ICE Support**: Includes platform normalization and package manager detection functions + - `determine_package_manager()` - Detects package format based on platform + - `normalize_platform_name()` - Maps specific platforms to generic categories (linux, macos, windows, unix) + - Conditional URL construction based on product type (chef-ice vs. standard products) ### PowerShell (install.ps1) - Supports: http_proxy - Windows platform support - TLS negotiation for older .NET versions - Generated via `lib/mixlib/install/generator/powershell.rb` +- **API Endpoint Selection**: Uses `base_server_uri` parameter to determine endpoint: + - If `base_server_uri` is empty and `license_id` is provided: + - Trial API: `https://chefdownload-trial.chef.io` (for `free-*` or `trial-*` prefixes) + - Commercial API: `https://chefdownload-commercial.chef.io` (for other license IDs) + - If `base_server_uri` is empty and no `license_id`: Omnitruck API `https://omnitruck.chef.io` + - If `base_server_uri` is set: Uses the provided URL (allows override) - **JSON API Response**: When `license_id` is provided: - Parses JSON responses with `ConvertFrom-Json` - Extracts `url` and `sha256` from JSON object - Automatically routes to trial or commercial API based on license_id prefix +- **Chef-ICE Support**: Simplified parameters for Windows + - Uses `p=windows`, `m=`, `pm=msi` parameters for chef-ice + - Conditional logic to select appropriate metadata URL format based on product type + - Handles both chef-ice and standard product URL construction ### Script Options - `download_url_override`: Direct URL instead of API lookup - `checksum`: SHA256 for verification - `install_strategy`: "once" to skip if already installed - `license_id`: License ID for commercial/trial API access (format: `free-*`, `trial-*`, or standard license ID) +- `base_api_url` (shell): Override API endpoint (optional, auto-detected from license_id if not provided) +- `base_server_uri` (PowerShell): Override API endpoint (optional, auto-detected from license_id if not provided) ## API Usage Patterns @@ -296,9 +366,12 @@ Mixlib::Install supports Chef's commercial and trial licensing APIs, which provi - **Trial API**: `https://chefdownload-trial.chef.io` - Used when `license_id` starts with `free-` or `trial-` - Returns JSON responses with download URLs + - **Restrictions**: Only `stable` channel and `latest` version supported + - Defaults are automatically enforced with warnings - **Commercial API**: `https://chefdownload-commercial.chef.io` - Used for standard license IDs - Returns JSON responses with download URLs + - No restrictions on channels or versions - **Traditional Omnitruck**: `https://omnitruck.chef.io` - Used when no `license_id` is provided - Returns text-based metadata responses @@ -317,6 +390,36 @@ Mixlib::Install supports Chef's commercial and trial licensing APIs, which provi sha256\tabc123... ``` +### Error Handling for Trial API Restrictions +The backend (`lib/mixlib/install/backend/package_router.rb`) includes enhanced error handling: +- Catches `Net::HTTPClientError` and `Net::HTTPServerError` during API calls +- Provides helpful error messages when trial API restrictions are violated: + - If trial license is detected but non-compliant settings are used (channel != :stable or version != :latest) + - Error message includes current settings and reminds user of trial API limitations +- Re-raises original error for other failure scenarios + +### License ID Detection Helper Methods (`lib/mixlib/install/dist.rb`) +```ruby +require 'mixlib/install/dist' + +# Check if license_id indicates trial API usage +Mixlib::Install::Dist.trial_license?('free-trial-123') # => true +Mixlib::Install::Dist.trial_license?('trial-abc-456') # => true +Mixlib::Install::Dist.trial_license?('commercial-xyz') # => false + +# Check if license_id indicates commercial API usage +Mixlib::Install::Dist.commercial_license?('commercial-xyz') # => true +Mixlib::Install::Dist.commercial_license?('free-trial-123') # => false +``` + +**Trial License Detection Logic**: +- Returns `true` if license_id starts with `free-` or `trial-` +- Returns `false` for nil, empty string, or other prefixes + +**Commercial License Detection Logic**: +- Returns `true` if license_id is present and NOT a trial license +- Returns `false` for nil, empty string, or trial licenses + ### Content-Disposition Header Handling Commercial and trial APIs return endpoint URLs that use HTTP Content-Disposition headers to specify the actual filename, rather than including the filename in the URL path. @@ -345,6 +448,176 @@ When adding or modifying commercial/trial API functionality: 1. Verify JSON parsing in both Bourne shell (sed) and PowerShell (ConvertFrom-Json) 1. Test filename extraction with various response header formats 1. Verify fallback filename construction for each platform type +1. Test chef-ice product with package_manager parameter +1. Verify platform normalization for chef-ice on all supported platforms +1. Test trial API automatic defaults enforcement (stable channel, latest version) + +### Test Patterns for Chef-ICE and Trial API +Key test patterns to follow (see `spec/unit/mixlib/install/generator_spec.rb` for examples): + +**Chef-ICE Shell Script Tests**: +```ruby +context "chef-ice with commercial API" do + let(:add_options) do + { + product_name: "chef-ice", + license_id: "test-license-key-123", + } + end + + it "includes package manager detection function" do + expect(install_script).to include("determine_package_manager()") + end + + it "includes platform normalization function" do + expect(install_script).to include("normalize_platform_name()") + end + + it "constructs chef-ice metadata URL with m, p, pm parameters" do + expect(install_script).to include('metadata_url="$base_api_url/$channel/$project/metadata?license_id=$license_id&v=$version&m=$machine&p=$platform_param&pm=$package_manager"') + end +end +``` + +**Chef-ICE PowerShell Tests**: +```ruby +context "chef-ice with commercial API for PowerShell" do + let(:add_options) do + { + product_name: "chef-ice", + shell_type: :ps1, + license_id: "test-license-key-123", + } + end + + it "includes simplified parameters for chef-ice on Windows" do + expect(install_script).to include('$platform_param = "windows"') + expect(install_script).to include('$package_manager = "msi"') + end + + it "constructs chef-ice metadata URL with m, p, pm parameters" do + expect(install_script).to include('$metadata_url = "$base_server_uri/$channel/$project/metadata?license_id=$license_id&v=$version&m=$architecture&p=$platform_param&pm=$package_manager"') + end +end +``` + +**Trial API Enforcement Tests**: +```ruby +it "defaults to stable channel when current channel is specified" do + expect do + mi = Mixlib::Install.new(product_name: "chef", channel: :current, license_id: "free-trial-abc-123") + expect(mi.options.channel).to eq :stable + end.to output(/WARNING: Trial API only supports 'stable' channel/).to_stderr +end + +it "defaults to latest version when specific version is specified" do + expect do + mi = Mixlib::Install.new(product_name: "chef", product_version: "15.0.0", license_id: "free-trial-abc-123") + expect(mi.options.product_version).to eq :latest + end.to output(/WARNING: Trial API only supports 'latest' version/).to_stderr +end +``` + +## Chef-ICE Product Support + +The `chef-ice` product (Chef Infra Client Enterprise, Chef 19+) requires special handling: + +### Key Characteristics: +- **Product Name**: `chef-ice` +- **Package Name**: `chef-ice` +- **Minimum Version**: Chef 19.x +- **API Compatibility**: Works with both commercial and trial APIs +- **URL Parameters**: Uses `m`, `p`, `pm` instead of standard `p`, `pv`, `m` format +- **Install Directories**: Uses Habitat package paths instead of Omnibus paths + +### Install Directory Constants (`lib/mixlib/install/dist.rb`): + +Chef products use different install directory structures depending on whether they're packaged with Omnibus or Habitat: + +**Omnibus Products** (chef, chefdk, etc.): +- Windows: `$env:systemdrive\opscode\{product}` +- Linux: `/opt/{product}` +- Constants: `OMNIBUS_WINDOWS_INSTALL_DIR`, `OMNIBUS_LINUX_INSTALL_DIR` + +**Habitat Products** (chef-ice): +- Windows: `$env:systemdrive\hab\pkgs\chef\chef-infra-client\*\*` +- Linux: `/hab/pkgs/chef/chef-infra-client/*/*` +- Constants: `HABITAT_WINDOWS_INSTALL_DIR`, `HABITAT_LINUX_INSTALL_DIR` + +**Implementation Details**: +- `OMNIBUS_WINDOWS_INSTALL_DIR = "opscode"` - Traditional Chef install base directory for Windows +- `OMNIBUS_LINUX_INSTALL_DIR = "/opt"` - Traditional Chef install base directory for Linux +- `HABITAT_WINDOWS_INSTALL_DIR = "hab\\pkgs"` - Habitat package directory for Windows +- `HABITAT_LINUX_INSTALL_DIR = "/hab/pkgs"` - Habitat package directory for Linux + +**Usage in Code**: +- `lib/mixlib/install.rb`: `root` and `current_version` methods check product name and use appropriate constants +- `lib/mixlib/install/script_generator.rb`: Sets `@root` based on product type after initialization +- `lib/mixlib/install/generator/base.rb`: Conditionally sets `context[:windows_dir]` for chef-ice + +The wildcard paths (`*/*`) in Habitat directories allow matching any version/release combination of the package. + +### URL Parameter Differences: +**Standard Products (chef, chef-backend, etc.)**: +``` +?p={platform}&pv={platform_version}&m={machine}&v={version}&license_id={id} +``` + +**Chef-ICE Product**: +``` +?v={version}&license_id={id}&m={machine}&p={normalized_platform}&pm={package_manager} +``` + +### Platform Normalization (`Util.normalize_platform_for_commercial`): +Chef-ICE uses generic platform categories: +- **linux**: el, centos, rhel, fedora, rocky, scientific, debian, ubuntu, linuxmint, raspbian, opensuse, sles, amazon +- **macos**: mac_os_x, macos +- **windows**: windows +- **unix**: freebsd, aix, solaris, smartos, omnios +- **Default**: linux (for unknown platforms) + +### Package Manager Detection (`Util.determine_package_manager`): +Automatically determines package format based on platform: +- **rpm**: el, centos, rhel, fedora, amazon, rocky, opensuse, sles, scientific +- **deb**: debian, ubuntu, linuxmint, raspbian +- **dmg**: mac_os_x, macos +- **msi**: windows +- **tar**: solaris, smartos, freebsd, aix, omnios +- **Default**: tar (for unknown platforms) + +### Implementation Locations: +- **Backend Logic**: `lib/mixlib/install/backend/package_router.rb` (lines 265-270) + - `create_artifact` method checks for `chef-ice` and constructs appropriate download URLs + - Implements platform normalization and package manager parameter addition +- **Utility Functions**: `lib/mixlib/install/util.rb` (lines 182-224) + - `determine_package_manager(platform)` - Returns package format (rpm, deb, dmg, msi, tar) + - `normalize_platform_for_commercial(platform)` - Maps platforms to generic categories +- **Shell Script**: `lib/mixlib/install/generator/bourne/scripts/fetch_metadata.sh` + - Includes `determine_package_manager()` and `normalize_platform_name()` shell functions + - Conditional metadata URL construction for chef-ice +- **PowerShell Script**: `lib/mixlib/install/generator/powershell/scripts/get_project_metadata.ps1` + - Simplified parameter handling for chef-ice on Windows + - Uses `p=windows`, `pm=msi` for chef-ice metadata queries +- **Root Directory Logic**: `lib/mixlib/install.rb` and `lib/mixlib/install/script_generator.rb` + - Methods check product name and conditionally use Habitat paths + - `root` method returns appropriate install directory path + - `current_version` method uses correct version-manifest.json path + +### Example Usage: +```ruby +options = { + product_name: 'chef-ice', + channel: :stable, + product_version: :latest, + platform: 'ubuntu', + platform_version: '20.04', + architecture: 'x86_64', + license_id: 'free-trial-abc-123' +} + +artifact = Mixlib::Install.new(options).artifact_info +# URL: https://chefdownload-trial.chef.io/stable/chef-ice/download?v=19.1.151&license_id=free-trial-abc-123&m=x86_64&p=linux&pm=deb +``` ## Common Pitfalls to Avoid @@ -357,8 +630,39 @@ When adding or modifying commercial/trial API functionality: 1. **Don't add dependencies without version constraints** - Especially for Ruby 2.6+ support 1. **Don't assume filename in URL** - Commercial/trial APIs use Content-Disposition headers 1. **Don't break temp file download approach** - Required for license_id support across all download methods +1. **Don't forget chef-ice special handling** - Different URL parameters and platform normalization +1. **Don't bypass trial API defaults** - Trial licenses must use stable channel and latest version 1. **Don't use emojis** - Never use emojis in code, comments, output messages, or documentation +### Common Issues and Solutions + +**Chef-ICE Installation Issues**: +- Ensure `package_manager` parameter is included in metadata URLs +- Verify platform normalization returns correct category (linux, macos, windows, unix) +- Check that Habitat install directories are used (not Omnibus paths) +- For Windows: Must use `pm=msi` parameter + +**Trial API Restrictions**: +- Trial licenses automatically default to stable channel with warning +- Trial licenses automatically default to latest version with warning +- Users cannot override these defaults for trial API +- Commercial licenses have no such restrictions + +**Content-Disposition Filename Extraction**: +- If filename extraction fails, fallback construction should work +- Test with multiple download tools (wget, curl, fetch, perl, python) +- Verify temp file approach doesn't break existing functionality +- Check that filename has correct extension for platform (.rpm, .deb, .msi, etc.) + +**API Endpoint Selection Issues**: +- Ensure `base_api_url`/`base_server_uri` conditional logic checks for empty/unset (not inverted) +- Shell scripts: Use `[ -z "$base_api_url" ]` to check if empty +- PowerShell scripts: Use `[string]::IsNullOrEmpty($base_server_uri)` to check if empty +- When set by user, respect the provided endpoint URL +- When unset, automatically determine based on license_id presence and prefix + + + ## Documentation Requirements When making changes: @@ -417,11 +721,34 @@ When making changes: ### Environment Variables - `EXTRA_PRODUCTS_FILE` - Path to custom product definitions - `http_proxy`, `https_proxy`, `ftp_proxy`, `no_proxy` - Proxy configuration +- `CHEF_LICENSE_KEY` - Fallback license ID for install scripts (if not provided via parameter) + +### Quick Reference: Chef-ICE vs Standard Products + +| Aspect | Standard Products (chef, chefdk, etc.) | Chef-ICE Product | +|--------|---------------------------------------|------------------| +| **Package System** | Omnibus | Habitat | +| **Install Dir (Windows)** | `C:\opscode\` | `C:\hab\pkgs\chef\chef-infra-client\*\*` | +| **Install Dir (Linux)** | `/opt/` | `/hab/pkgs/chef/chef-infra-client/*/*` | +| **URL Parameters** | `?p=&pv=&m=&v=&license_id=` | `?v=&license_id=&m=&p=&pm=` | +| **Platform Values** | Specific (ubuntu, el, centos, etc.) | Normalized (linux, macos, windows, unix) | +| **Requires PM Param** | No | Yes (rpm, deb, msi, dmg, tar) | +| **Min Version** | Varies by product | Chef 19+ | + +### Quick Reference: License Types + +| License Type | ID Format | API Endpoint | Channel | Version | Auto-Defaults | +|-------------|-----------|--------------|---------|---------|---------------| +| **Trial** | `free-*` or `trial-*` | https://chefdownload-trial.chef.io | stable only | latest only | Yes (with warnings) | +| **Commercial** | Any other format | https://chefdownload-commercial.chef.io | Any | Any | No | +| **Open Source** | None | https://omnitruck.chef.io | Any | Any | No | --- **Remember**: When in doubt about Ruby version compatibility, check the Gemfile and gemspec for version-specific patterns, and test with Ruby 2.6+ when possible. The goal is maximum compatibility (Ruby 2.6+) without sacrificing functionality. +For chef-ice products, always verify that platform normalization and package manager detection work correctly for the target platform before deploying changes. + ### Ruby 2.6+ Feature Reference #### Safe to Use (Ruby 2.6+) @@ -432,7 +759,7 @@ When making changes: - `Hash#fetch_values` - `Hash#to_proc` - Frozen string literal pragma: `# frozen_string_literal: true` -- Endless ranges: `(1..)` +- Endless ranges: `(1..)` - `Enumerable#chain` - `Kernel#then` - `Integer#digits` diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 2be2ada8..cc7debcc 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -51,6 +51,9 @@ jobs: run: bundle exec mixlib-install download chef - name: Chef Licensed download Test run: bundle exec mixlib-install download chef -L free-79df705d-b685-419a-8b68-88401f74ff72-3999 + - name: Chef ICE download Test + if: runner.os != 'macOS' + run: bundle exec mixlib-install download chef-ice -L free-79df705d-b685-419a-8b68-88401f74ff72-3999 test-install-command: runs-on: ${{ matrix.os }} strategy: @@ -353,3 +356,136 @@ jobs: "@ | Out-File -FilePath test_with_license.rb -Encoding utf8 - name: Test ScriptGenerator with license_id run: bundle exec ruby test_with_license.rb + test-install-command-chef-ice: + name: Test chef-ice install command generation and execution + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - name: Enable long paths on Windows + if: runner.os == 'Windows' + run: | + # Enable Windows long paths + New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force -ErrorAction SilentlyContinue + # Enable Git long paths + git config --global core.longpaths true + - name: Checkout code + uses: actions/checkout@v6 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - name: Generate install command script (Linux/macOS) + if: runner.os != 'Windows' + run: | + # Generate the install command using Ruby API + bundle exec ruby -e " + require 'bundler/setup' + require 'mixlib/install' + installer = Mixlib::Install.new( + channel: :stable, + product_name: 'chef-ice', + license_id: 'free-79df705d-b685-419a-8b68-88401f74ff72-3999' + ) + puts installer.install_command + " > install_cmd.sh + cat install_cmd.sh + chmod +x install_cmd.sh + - name: Run install command script (Linux/macOS) + if: runner.os != 'Windows' + run: | + # Run the install command with sudo + sudo bash install_cmd.sh + # Verify chef-client was installed (chef-ice installs chef-infra-client) + which chef-client + chef-client --version + - name: Generate install command script (Windows) + if: runner.os == 'Windows' + run: | + # Generate the install command using Ruby API (one-liner for PowerShell) + bundle exec ruby -e "require 'bundler/setup'; require 'mixlib/install'; installer = Mixlib::Install.new(channel: :stable, product_name: 'chef-ice', license_id: 'free-79df705d-b685-419a-8b68-88401f74ff72-3999', shell_type: :ps1); File.write('install_cmd.ps1', installer.install_command)" + Get-Content install_cmd.ps1 + - name: Run install command script (Windows) + if: runner.os == 'Windows' + run: | + # Run the install command + .\install_cmd.ps1 + # Refresh PATH from registry to pick up newly installed chef-client + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + # Verify chef-client was installed + Get-Command chef-client + chef-client --version + test-install-sh-chef-ice: + name: Test chef-ice install.sh generation and execution + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - name: Generate install.sh script for chef-ice + run: | + bundle exec ruby -e " + require 'bundler/setup' + require 'mixlib/install' + installer = Mixlib::Install.new( + channel: :stable, + product_name: 'chef-ice', + license_id: 'free-79df705d-b685-419a-8b68-88401f74ff72-3999' + ) + File.write('install.sh', installer.install_command) + " + chmod +x install.sh + echo "Generated install.sh:" + cat install.sh + - name: Run install.sh script + run: | + sudo ./install.sh + + # Verify chef-client was installed (chef-ice installs chef-infra-client) + which chef-client + chef-client --version + test-install-ps1-chef-ice: + name: Test chef-ice install.ps1 generation and execution + runs-on: windows-latest + steps: + - name: Enable long paths on Windows + run: | + # Enable Windows long paths + New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force -ErrorAction SilentlyContinue + # Enable Git long paths + git config --global core.longpaths true + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true + - name: Generate install.ps1 script for chef-ice + run: | + bundle exec ruby -e "require 'bundler/setup'; require 'mixlib/install'; installer = Mixlib::Install.new(channel: :stable, product_name: 'chef-ice', license_id: 'free-79df705d-b685-419a-8b68-88401f74ff72-3999', shell_type: :ps1); File.write('install.ps1', installer.install_command)" + Write-Host "Generated install.ps1:" + Get-Content install.ps1 + - name: Run install.ps1 script + run: | + # Import the module and call the install function + . .\install.ps1 + install + # Refresh PATH from registry to pick up newly installed chef-client + $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + # Verify chef-client was installed + Get-Command chef-client + chef-client --version diff --git a/README.md b/README.md index e1f9336c..53f77c5b 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,110 @@ Then set an environment variable to load them like this on linux: Calls to mixlib-install now allow to target your new product, assuming the api_url match pacakges api for `///versions` and `////artifacts` endpoints. +## Licensed API Usage (Commercial and Trial) + +Mixlib::Install supports both commercial and trial API endpoints for licensed Chef products. These APIs require a valid license ID and have specific restrictions. + +### Trial API + +The trial API is designed for evaluation purposes and has the following restrictions: + +- **Endpoint**: `https://chefdownload-trial.chef.io` +- **License ID Format**: Must start with `free-` or `trial-` +- **Channel Restriction**: Only `stable` channel is supported +- **Version Restriction**: Only `latest` version is supported + +When using a trial license ID, mixlib-install will **automatically** default to `stable` channel and `latest` version, displaying warnings if other values are provided: + +```ruby +options = { + product_name: 'chef', + channel: :current, # Will be changed to :stable with warning + product_version: '18.5.0', # Will be changed to :latest with warning + license_id: 'free-trial-abc-123' +} + +mi = Mixlib::Install.new(options) +# WARNING: Trial API only supports 'stable' channel. Changing from 'current' to 'stable'. +# WARNING: Trial API only supports 'latest' version. Changing from '18.5.0' to 'latest'. + +mi.options.channel # => :stable +mi.options.product_version # => :latest +``` + +### Commercial API + +The commercial API provides full access to all channels and versions: + +- **Endpoint**: `https://chefdownload-commercial.chef.io` +- **License ID Format**: Any valid commercial license ID (not starting with `free-` or `trial-`) +- **Channel Restriction**: None - all channels supported (`stable`, `current`, `unstable`) +- **Version Restriction**: None - all versions supported + +```ruby +options = { + product_name: 'chef', + channel: :current, + product_version: '18.5.0', + license_id: 'commercial-license-key-123' +} + +mi = Mixlib::Install.new(options) +# No warnings or defaults applied +mi.options.channel # => :current +mi.options.product_version # => '18.5.0' +``` + +### Chef-ICE Product + +The `chef-ice` product requires additional parameters (`m`, `p`, `pm`) and works with both commercial and trial APIs: + +```ruby +options = { + product_name: 'chef-ice', + channel: :stable, + product_version: :latest, + platform: 'ubuntu', + platform_version: '20.04', + architecture: 'x86_64', + license_id: 'free-trial-abc-123' # Trial API +} + +artifact = Mixlib::Install.new(options).artifact_info +artifact.url +# => "https://chefdownload-trial.chef.io/stable/chef-ice/download?v=19.1.151&license_id=free-trial-abc-123&m=x86_64&p=linux&pm=deb" +``` + +### Static Script Methods + +The static methods `Mixlib::Install.install_sh()` and `Mixlib::Install.install_ps1()` also enforce trial API defaults: + +```ruby +# Trial API defaults applied to generated script +script = Mixlib::Install.install_sh( + license_id: 'free-trial-xyz', + channel: :current, # Will be changed to :stable with warning + version: '18.0.0' # Will be changed to :latest with warning +) +# WARNING: Trial API only supports 'stable' channel. Changing from 'current' to 'stable'. +# WARNING: Trial API only supports 'latest' version. Changing from '18.0.0' to 'latest'. +``` + +### License ID Detection + +You can check if a license ID is for trial or commercial API: + +```ruby +require 'mixlib/install/dist' + +Mixlib::Install::Dist.trial_license?('free-trial-123') # => true +Mixlib::Install::Dist.trial_license?('trial-abc-456') # => true +Mixlib::Install::Dist.trial_license?('commercial-xyz') # => false + +Mixlib::Install::Dist.commercial_license?('commercial-xyz') # => true +Mixlib::Install::Dist.commercial_license?('free-trial-123') # => false +``` + ## Development VCR is a tool that helps cache and replay http responses. When these responses change or when you add more tests you might need to update cached responses. Check out [spec_helper.rb](https://github.com/chef/mixlib-install/blob/master/spec/spec_helper.rb) for instructions on how to do this. diff --git a/generate_install_scripts.sh b/generate_install_scripts.sh new file mode 100755 index 00000000..7167c392 --- /dev/null +++ b/generate_install_scripts.sh @@ -0,0 +1,177 @@ +#!/bin/bash + +# +# Script to generate Chef install scripts using mixlib-install +# +# Usage: ./generate_install_scripts.sh [options] +# +# Options: +# -L, --license-key KEY - Chef license key for commercial downloads +# (optional, uses CHEF_LICENSE_KEY env var if not provided) +# -b, --base-url URL - Base URL for package downloads (optional) +# -p, --product NAME - Product name (default: chef) +# -c, --channel NAME - Channel (default: stable) +# -v, --version VER - Product version (default: latest) +# -o, --output DIR - Output directory (default: current directory) +# -h, --help - Show this help message +# + +set -e + +# Default values +PRODUCT_NAME="chef" +CHANNEL="stable" +VERSION="latest" +OUTPUT_DIR="." +LICENSE_KEY="" +BASE_URL="" + +# Parse command line arguments +show_usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -L, --license-key KEY Chef license key for commercial downloads" + echo " (optional, uses CHEF_LICENSE_KEY env var if not provided)" + echo " -b, --base-url URL Base URL for package downloads (optional)" + echo " -p, --product NAME Product name (default: chef)" + echo " -c, --channel NAME Channel: stable, current, or unstable (default: stable)" + echo " -v, --version VER Product version (default: latest)" + echo " -o, --output DIR Output directory (default: current directory)" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 -L my-license-key-123" + echo " $0 -L my-license-key-123 -p chef-workstation -v 24.2.1058" + echo " $0 -o /tmp/scripts -c current" + echo " $0 -b https://custom-repo.example.com" + echo " CHEF_LICENSE_KEY=my-key $0 -p chef-workstation" + exit 0 +} + +# Check if help is requested +if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then + show_usage +fi + +# Parse options +while [[ $# -gt 0 ]]; do + case $1 in + -L|--license-key) + LICENSE_KEY="$2" + shift 2 + ;; + -b|--base-url) + BASE_URL="$2" + shift 2 + ;; + -p|--product) + PRODUCT_NAME="$2" + shift 2 + ;; + -c|--channel) + CHANNEL="$2" + shift 2 + ;; + -v|--version) + VERSION="$2" + shift 2 + ;; + -o|--output) + OUTPUT_DIR="$2" + shift 2 + ;; + -h|--help) + show_usage + ;; + *) + echo "Error: Unknown option: $1" + show_usage + ;; + esac +done + +# Use CHEF_LICENSE_KEY environment variable if license key not provided via option +if [ -z "$LICENSE_KEY" ] && [ -n "${CHEF_LICENSE_KEY:-}" ]; then + LICENSE_KEY="$CHEF_LICENSE_KEY" + echo "Using license key from CHEF_LICENSE_KEY environment variable" +fi + +# Create output directory if it doesn't exist +mkdir -p "$OUTPUT_DIR" + +# Check if mixlib-install gem is installed +echo "Checking for mixlib-install gem..." +if ! gem list -i mixlib-install > /dev/null 2>&1; then + echo "mixlib-install gem not found. Installing..." + gem build mixlib-install.gemspec + gem install mixlib-install-*.gem + echo "✓ mixlib-install gem installed successfully" +else + echo "✓ mixlib-install gem is already installed" +fi + +# Generate install.sh script for Linux/Unix +echo "" +echo "Generating install.sh for $PRODUCT_NAME (channel: $CHANNEL, version: $VERSION)..." + +ruby -I "lib" -e " +require 'mixlib/install' + +context = {} +context[:license_id] = '$LICENSE_KEY' unless '$LICENSE_KEY'.empty? +context[:base_url] = '$BASE_URL' unless '$BASE_URL'.empty? + +script = Mixlib::Install.install_sh(context) + +File.write('$OUTPUT_DIR/install.sh', script) +puts '✓ install.sh generated successfully' +" + +# Make the script executable +chmod +x "$OUTPUT_DIR/install.sh" + +# Generate install.ps1 script for Windows +echo "" +echo "Generating install.ps1 for $PRODUCT_NAME (channel: $CHANNEL, version: $VERSION)..." + +ruby -I "lib" -e " +require 'mixlib/install' + +context = {} +context[:license_id] = '$LICENSE_KEY' unless '$LICENSE_KEY'.empty? +context[:base_url] = '$BASE_URL' unless '$BASE_URL'.empty? + +script = Mixlib::Install.install_ps1(context) + +File.write('$OUTPUT_DIR/install.ps1', script) +puts '✓ install.ps1 generated successfully' +" + +# Summary +echo "" +echo "================================================" +echo "Scripts generated successfully!" +echo "================================================" +echo "Product: $PRODUCT_NAME" +echo "Channel: $CHANNEL" +echo "Version: $VERSION" +if [ -n "$LICENSE_KEY" ]; then + echo "License Key: ${LICENSE_KEY:0:10}..." # Show only first 10 chars +else + echo "License Key: Not provided" +fi +if [ -n "$BASE_URL" ]; then + echo "Base URL: $BASE_URL" +fi +echo "" +echo "Output files:" +echo " - $OUTPUT_DIR/install.sh" +echo " - $OUTPUT_DIR/install.ps1" +echo "" +if [ -n "$LICENSE_KEY" ]; then + echo "The license key has been embedded in the generated scripts." +else + echo "Note: No license key provided. Scripts will check for CHEF_LICENSE_KEY environment variable at runtime." +fi +echo "You can now use these scripts to install $PRODUCT_NAME on Linux/Unix and Windows systems." diff --git a/lib/mixlib/install.rb b/lib/mixlib/install.rb index e5c3cf95..53814aad 100644 --- a/lib/mixlib/install.rb +++ b/lib/mixlib/install.rb @@ -103,6 +103,7 @@ def download_artifact(directory = Dir.pwd) uri = URI.parse(artifact.url) filename = nil final_body = nil + final_uri = uri Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http| # Build the request path including query string @@ -112,23 +113,27 @@ def download_artifact(directory = Dir.pwd) # Get the response, following redirects response = http.request_get(request_path) + # Try to extract filename from Content-Disposition in initial response + if response["content-disposition"] + filename = response["content-disposition"][/filename="?([^"]+)"?/, 1] + end + # Follow redirects redirect_limit = 5 while response.is_a?(Net::HTTPRedirection) && redirect_limit > 0 redirect_uri = URI.parse(response["location"]) # Handle relative redirects redirect_uri = uri + redirect_uri if redirect_uri.relative? + final_uri = redirect_uri Net::HTTP.start(redirect_uri.host, redirect_uri.port, use_ssl: redirect_uri.scheme == "https") do |redirect_http| redirect_path = redirect_uri.path redirect_path += "?#{redirect_uri.query}" if redirect_uri.query response = redirect_http.request_get(redirect_path) - # Try to get filename from Content-Disposition or final URL - if response["content-disposition"] + # Try to get filename from Content-Disposition in redirect response + if response["content-disposition"] && filename.nil? filename = response["content-disposition"][/filename="?([^"]+)"?/, 1] - else - filename = File.basename(redirect_uri.path) end end @@ -136,9 +141,23 @@ def download_artifact(directory = Dir.pwd) end final_body = response.body + + # Try Content-Disposition from final successful response + if response["content-disposition"] && filename.nil? + filename = response["content-disposition"][/filename="?([^"]+)"?/, 1] + end end - # Use the extracted filename or fall back to basename of original URL + # Fallback: extract filename from final URL path (works for direct package URLs) + if filename.nil? + path_filename = File.basename(final_uri.path.split("?").first) + # Only use path filename if it looks like a package file + if /\.(rpm|deb|pkg|msi|dmg|bff|p5p|sh|tar|gz|appx)$/.match?(path_filename) + filename = path_filename + end + end + + # Final fallback: use basename of original URL filename ||= File.basename(uri.path) file = File.join(directory, filename) @@ -158,10 +177,19 @@ def download_artifact(directory = Dir.pwd) def root # This only works for chef and chefdk but they are the only projects # we are supporting as of now. - if options.for_ps1? - "$env:systemdrive\\#{Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR}\\#{options.product_name}" + # chef-ice uses Habitat install directories + if options.product_name.casecmp("chef-ice") == 0 + if options.for_ps1? + "$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 else - "#{Mixlib::Install::Dist::OMNIBUS_LINUX_INSTALL_DIR}/#{options.product_name}" + if options.for_ps1? + "$env:systemdrive\\#{Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR}\\#{options.product_name}" + else + "#{Mixlib::Install::Dist::OMNIBUS_LINUX_INSTALL_DIR}/#{options.product_name}" + end end end @@ -175,10 +203,19 @@ def current_version # 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. - version_manifest_file = if options.for_ps1? - "$env:systemdrive\\#{Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR}\\#{options.product_name}\\version-manifest.json" + # chef-ice uses Habitat install directories + version_manifest_file = if options.product_name.casecmp("chef-ice") == 0 + if options.for_ps1? + "$env:systemdrive\\#{Mixlib::Install::Dist::HABITAT_WINDOWS_INSTALL_DIR}\\chef\\chef-infra-client\\*\\*\\version-manifest.json" + else + "#{Mixlib::Install::Dist::HABITAT_LINUX_INSTALL_DIR}/chef/chef-infra-client/*/*/version-manifest.json" + end else - "#{Mixlib::Install::Dist::OMNIBUS_LINUX_INSTALL_DIR}/#{options.product_name}/version-manifest.json" + if options.for_ps1? + "$env:systemdrive\\#{Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR}\\#{options.product_name}\\version-manifest.json" + else + "/opt/#{options.product_name}/version-manifest.json" + end end if File.exist? version_manifest_file diff --git a/lib/mixlib/install/backend/base.rb b/lib/mixlib/install/backend/base.rb index 0feed828..2b7f5db0 100644 --- a/lib/mixlib/install/backend/base.rb +++ b/lib/mixlib/install/backend/base.rb @@ -84,9 +84,23 @@ def platform_filters_available? def filter_artifacts(artifacts) return artifacts unless platform_filters_available? + # For chef-ice, normalize the platform for comparison since chef-ice uses + # normalized platform names (linux, macos, windows) in the API response + comparison_platform = if options.product_name == "chef-ice" + Util.normalize_platform_for_commercial(options.platform) + else + options.platform + end + # First filter the artifacts based on the platform and architecture artifacts.select! do |a| - a.platform == options.platform && a.architecture == options.architecture + a.platform == comparison_platform && a.architecture == options.architecture + end + + # For chef-ice, platform_version is not used (it's empty in responses) + # so we should return the first match if available + if options.product_name == "chef-ice" + return artifacts.first if artifacts.any? end # Now we are going to filter based on platform_version. diff --git a/lib/mixlib/install/backend/package_router.rb b/lib/mixlib/install/backend/package_router.rb index 4c83638f..10f07842 100644 --- a/lib/mixlib/install/backend/package_router.rb +++ b/lib/mixlib/install/backend/package_router.rb @@ -137,23 +137,47 @@ def artifacts_for_version(version) # Commercial/trial APIs use the packages endpoint which returns metadata for all platforms query = "v=#{version}" packages_hash = get("/#{options.channel}/#{omnibus_project}/packages?#{query}") - # Response is a nested hash: platform -> platform_version -> architecture -> package_info - # Flatten it to an array of package metadata objects + # Response structure differs between products: + # - For chef-ice: platform -> architecture -> package_manager -> package_info + # - For other products: platform -> platform_version -> architecture -> package_info results = [] - packages_hash.each do |platform, platform_versions| - platform_versions.each do |platform_version, architectures| - architectures.each do |arch, pkg_info| - results << { - "omnibus.version" => pkg_info["version"], - "omnibus.platform" => platform, - "omnibus.platform_version" => platform_version, - "omnibus.architecture" => arch, - "omnibus.project" => omnibus_project, - "omnibus.license" => "Apache-2.0", - "omnibus.sha256" => pkg_info["sha256"], - "omnibus.sha1" => pkg_info.fetch("sha1", ""), - "omnibus.md5" => pkg_info.fetch("md5", ""), - } + if omnibus_project == "chef-ice" + # chef-ice structure: platform -> architecture -> package_manager -> package_info + packages_hash.each do |platform, architectures| + architectures.each do |arch, package_managers| + package_managers.each do |pm, pkg_info| + results << { + "omnibus.version" => pkg_info["version"], + "omnibus.platform" => platform, + "omnibus.platform_version" => "", + "omnibus.architecture" => arch, + "omnibus.project" => omnibus_project, + "omnibus.license" => "Apache-2.0", + "omnibus.sha256" => pkg_info["sha256"], + "omnibus.sha1" => pkg_info.fetch("sha1", ""), + "omnibus.md5" => pkg_info.fetch("md5", ""), + "omnibus.package_manager" => pm, + } + end + end + end + else + # Standard structure: platform -> platform_version -> architecture -> package_info + packages_hash.each do |platform, platform_versions| + platform_versions.each do |platform_version, architectures| + architectures.each do |arch, pkg_info| + results << { + "omnibus.version" => pkg_info["version"], + "omnibus.platform" => platform, + "omnibus.platform_version" => platform_version, + "omnibus.architecture" => arch, + "omnibus.project" => omnibus_project, + "omnibus.license" => "Apache-2.0", + "omnibus.sha256" => pkg_info["sha256"], + "omnibus.sha1" => pkg_info.fetch("sha1", ""), + "omnibus.md5" => pkg_info.fetch("md5", ""), + } + end end end end @@ -198,6 +222,16 @@ def get(url) res = http.request(create_http_request(full_path)) res.value JSON.parse(res.body) + rescue Net::HTTPClientError, Net::HTTPServerError => e + # Provide helpful error messages for licensed API failures + if use_trial_api? + if options.channel != :stable || (options.product_version != :latest && options.product_version.to_sym != :latest) + raise "Trial API only supports stable channel and latest version. " \ + "Current settings: channel=#{options.channel}, version=#{options.product_version}. " \ + "Error: #{e.message}" + end + end + raise e end def create_http_request(full_path) @@ -251,7 +285,16 @@ def create_artifact(artifact_map) pv_param = platform_version m_param = Util.normalize_architecture(artifact_map["omnibus.architecture"]) v_param = artifact_map["omnibus.version"] - download_url = "#{endpoint}/#{options.channel}/#{omnibus_project}/download?p=#{p_param}&pv=#{pv_param}&m=#{m_param}&v=#{v_param}&license_id=#{options.license_id}" + + # For chef-ice, use normalized platform names and add package manager parameter + if omnibus_project == "chef-ice" + p_param = Util.normalize_platform_for_commercial(platform) + # Use package_manager from artifact_map if available, otherwise determine it + pm_param = artifact_map.fetch("omnibus.package_manager", Util.determine_package_manager(options.platform)) + download_url = "#{endpoint}/#{options.channel}/#{omnibus_project}/download?v=#{v_param}&license_id=#{options.license_id}&m=#{m_param}&p=#{p_param}&pm=#{pm_param}" + else + download_url = "#{endpoint}/#{options.channel}/#{omnibus_project}/download?p=#{p_param}&pv=#{pv_param}&m=#{m_param}&v=#{v_param}&license_id=#{options.license_id}" + end else base_url = if use_compat_download_url_endpoint?(platform, platform_version) COMPAT_DOWNLOAD_URL_ENDPOINT diff --git a/lib/mixlib/install/generator/base.rb b/lib/mixlib/install/generator/base.rb index 0743b61c..48d6a761 100644 --- a/lib/mixlib/install/generator/base.rb +++ b/lib/mixlib/install/generator/base.rb @@ -49,21 +49,12 @@ def self.get_script(name, context = {}) if File.exist? "#{script_path}.erb" # Default values to use incase they are not set in the context context[:project_name] ||= Mixlib::Install::Dist::PROJECT_NAME.freeze - context[:base_url] ||= if context[:license_id] - if Mixlib::Install::Dist.trial_license?(context[:license_id]) - Mixlib::Install::Dist::TRIAL_API_ENDPOINT.freeze - else - Mixlib::Install::Dist::COMMERCIAL_API_ENDPOINT.freeze - end - else - Mixlib::Install::Dist::OMNITRUCK_ENDPOINT.freeze - end context[:default_product] ||= Mixlib::Install::Dist::DEFAULT_PRODUCT.freeze context[:bug_url] ||= Mixlib::Install::Dist::BUG_URL.freeze 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] ||= Mixlib::Install::Dist::OMNIBUS_WINDOWS_INSTALL_DIR.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[: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/bourne.rb b/lib/mixlib/install/generator/bourne.rb index a8d8f577..5ea2651d 100644 --- a/lib/mixlib/install/generator/bourne.rb +++ b/lib/mixlib/install/generator/bourne.rb @@ -49,7 +49,7 @@ def install_command install_command << get_script("check_product.sh") install_command << get_script("platform_detection.sh") install_command << get_script("proxy_env.sh") - install_command << get_script("fetch_metadata.sh", license_id: options.license_id) + install_command << get_script("fetch_metadata.sh", license_id: options.license_id, base_url: options.base_url) install_command << get_script("fetch_package.sh") install_command << get_script("install_package.sh") @@ -62,6 +62,7 @@ def render_variables version=#{options.product_version} channel=#{options.channel} #{"license_id=#{options.license_id}" if options.license_id && !options.license_id.to_s.empty?} +#{"base_api_url=#{options.base_url}" if options.base_url && !options.base_url.to_s.empty?} EOS # Check for CHEF_LICENSE_KEY in execution environment if not already set vars += </install.sh<%= base_url && base_url.include?("chefdownload") ? "?license_id=$CHEF_LICENSE_KEY" : "" %>' | sudo bash -s -- -P chef -c stable +# +# Gets the download url and SHA256 checksum for the latest stable release of Chef Workstation. +# EXAMPLE +# curl -L '<%= base_url || "https://chefdownload-commercial.chef.io" %>/install.sh<%= base_url && base_url.include?("chefdownload") ? "?license_id=$CHEF_LICENSE_KEY" : "" %>' | sudo bash -s -- -P chef-workstation -c stable +# # Inputs: +# $base_api_url: # $channel: # $project: # $version: @@ -17,31 +25,93 @@ # $sha256: ############ +# Determine package manager based on platform for commercial API +determine_package_manager() { + case "$platform" in + el|centos|rhel|fedora|amazon|rocky|opensuse*|sles|scientific) + echo "rpm" + ;; + debian|ubuntu|linuxmint|raspbian) + echo "deb" + ;; + mac_os_x|macos|solaris*|smartos|freebsd|aix|omnios) + echo "tar" + ;; + *) + echo "tar" + ;; + esac +} + +# Normalize platform name for commercial API +normalize_platform_name() { + case "$platform" in + el|centos|rhel|fedora|rocky|scientific) + echo "linux" + ;; + mac_os_x|macos) + echo "macos" + ;; + debian|ubuntu|linuxmint|raspbian) + echo "linux" + ;; + freebsd|aix|solaris*|smartos|omnios) + echo "unix" + ;; + *) + # For anything else, use linux as default + echo "linux" + ;; + esac +} + if [ -z "$download_url_override" ]; then echo "Getting information for $project $channel $version for $platform..." metadata_filename="$tmp_dir/metadata.txt" +<% if base_url && !base_url.to_s.empty? %> + # Set base_api_url from option if not already set via CLI parameter + if [ -z "$base_api_url" ]; then + base_api_url="<%= base_url %>" + fi +<% end %> # Use commercial API if license_id is provided, otherwise use omnitruck + if [ -z "$base_api_url" ]; then + if [ -n "$license_id" ]; then + # Check if license_id starts with 'free-' or 'trial-' for trial API + case "$license_id" in + free-*|trial-*) + # Trial API endpoint + base_api_url="<%= Mixlib::Install::Dist::TRIAL_API_ENDPOINT %>" + ;; + *) + # Commercial API endpoint + base_api_url="<%= Mixlib::Install::Dist::COMMERCIAL_API_ENDPOINT %>" + ;; + esac + else + # Omnitruck endpoint + base_api_url="<%= Mixlib::Install::Dist::OMNITRUCK_ENDPOINT %>" + fi + fi + + # For chef-ice product, add platform (p), machine (m), and package_manager (pm) parameters if [ -n "$license_id" ]; then - # Check if license_id starts with 'free-' or 'trial-' for trial API - case "$license_id" in - free-*|trial-*) - # Trial API endpoint - base_api_url="https://chefdownload-trial.chef.io" - ;; - *) - # Commercial API endpoint - base_api_url="https://chefdownload-commercial.chef.io" - ;; - esac - metadata_url="$base_api_url/$channel/$project/metadata?v=$version&p=$platform&pv=$platform_version&m=$machine&license_id=$license_id" + if [ "$project" = "chef-ice" ]; then + package_manager=$(determine_package_manager) + platform_param=$(normalize_platform_name) + metadata_url="$base_api_url/$channel/$project/metadata?license_id=$license_id&v=$version&m=$machine&p=$platform_param&pm=$package_manager" + else + # For other products, use standard parameters + metadata_url="$base_api_url/$channel/$project/metadata?license_id=$license_id&v=$version&p=$platform&pv=$platform_version&m=$machine" + fi else - # Omnitruck endpoint - metadata_url="<%= base_url %>/$channel/$project/metadata?v=$version&p=$platform&pv=$platform_version&m=$machine" + # Omnitruck URL parameters without license_id + metadata_url="$base_api_url/$channel/$project/metadata?v=$version&p=$platform&pv=$platform_version&m=$machine" fi - do_download "$metadata_url" "$metadata_filename" + do_download "$metadata_url" "$metadata_filename" cat "$metadata_filename" @@ -85,8 +155,8 @@ if [ -z "$download_url_override" ]; then fi else download_url=$download_url_override - # Set sha256 to empty string if checksum not set - sha256=${checksum=""} + # Set sha256 to checksum value if provided, empty otherwise + sha256="${checksum:-}" fi ############ diff --git a/lib/mixlib/install/generator/bourne/scripts/fetch_package.sh b/lib/mixlib/install/generator/bourne/scripts/fetch_package.sh index aa303a65..329a5bdc 100644 --- a/lib/mixlib/install/generator/bourne/scripts/fetch_package.sh +++ b/lib/mixlib/install/generator/bourne/scripts/fetch_package.sh @@ -17,8 +17,28 @@ # For licensed APIs (commercial/trial), the URL is an endpoint, not a direct file URL # The actual filename will come from the Content-Disposition header -if [ -n "$license_id" ]; then - # Use content-disposition to get the filename +# Also check if download_url_override is used with a URL that doesn't have a filename +# (e.g., ends with /download or /download?params instead of /package-1.2.3.rpm) + +# Function to check if URL path contains a valid package filename +has_package_filename() { + url_path=`echo "$1" | sed -e 's/?.*//' -e 's/^.*\///'` + # Check if the path segment has a package extension + case "$url_path" in + *.rpm|*.deb|*.pkg|*.msi|*.dmg|*.bff|*.p5p|*.solaris|*.sh) + return 0 # has valid filename + ;; + *) + return 1 # no valid filename + ;; + esac +} + +# Determine if we need to use content-disposition based on license_id or URL structure +if [ -n "$license_id" ] || ! has_package_filename "$download_url"; then + # Use content-disposition to get the filename if: + # 1. license_id is set (commercial/trial API) + # 2. URL doesn't contain a package filename (e.g., ends with /download or /download?params) use_content_disposition="true" # We don't know the filename yet - it will come from Content-Disposition # Just set the download directory diff --git a/lib/mixlib/install/generator/bourne/scripts/script_cli_parameters.sh.erb b/lib/mixlib/install/generator/bourne/scripts/script_cli_parameters.sh.erb index 44bcb37f..a3a90645 100644 --- a/lib/mixlib/install/generator/bourne/scripts/script_cli_parameters.sh.erb +++ b/lib/mixlib/install/generator/bourne/scripts/script_cli_parameters.sh.erb @@ -25,6 +25,7 @@ do case "$opt" in v) version="$OPTARG";; + b) base_api_url="$OPTARG";; c) channel="$OPTARG";; p) channel="current";; # compat for prerelease option n) channel="current";; # compat for nightlies option @@ -37,7 +38,7 @@ do L) license_id="$OPTARG";; \?) # unknown flag echo >&2 \ - "usage: $0 [-P project] [-c release_channel] [-v version] [-f filename | -d download_dir] [-s install_strategy] [-l download_url_override] [-a checksum] [-L license_id]" + "usage: $0 [-P project] [-b base_api_url] [-c release_channel] [-v version] [-f filename | -d download_dir] [-s install_strategy] [-l download_url_override] [-a checksum] [-L license_id]" exit 1;; esac done @@ -45,6 +46,6 @@ done shift `expr $OPTIND - 1` # Use CHEF_LICENSE_KEY environment variable if license_id not provided via CLI -if [ -z "$license_id" ] && [ -n "${CHEF_LICENSE_KEY:-}" ]; then +if [ -z "$license_id" ] && [ -n "$CHEF_LICENSE_KEY" ]; then license_id="$CHEF_LICENSE_KEY" fi diff --git a/lib/mixlib/install/generator/powershell.rb b/lib/mixlib/install/generator/powershell.rb index 7d4dcb38..5db4d52c 100644 --- a/lib/mixlib/install/generator/powershell.rb +++ b/lib/mixlib/install/generator/powershell.rb @@ -46,7 +46,7 @@ def self.script_base_path 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) + 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_command = [] install_command << ps1_modularize(install_project_module.join("\n"), "Installer-Module") @@ -71,6 +71,7 @@ def render_command cmd << " -version #{options.product_version}" cmd << " -channel #{options.channel}" cmd << " -architecture #{options.architecture}" if options.architecture + cmd << " -base_server_uri '#{options.base_url}'" if options.base_url && !options.base_url.to_s.empty? cmd << " -license_id #{options.license_id}" if options.license_id && !options.license_id.to_s.empty? cmd << install_command_params if options.install_command_options cmd << "\n" diff --git a/lib/mixlib/install/generator/powershell/scripts/get_project_metadata.ps1.erb b/lib/mixlib/install/generator/powershell/scripts/get_project_metadata.ps1.erb index d915c4d7..5e840a81 100644 --- a/lib/mixlib/install/generator/powershell/scripts/get_project_metadata.ps1.erb +++ b/lib/mixlib/install/generator/powershell/scripts/get_project_metadata.ps1.erb @@ -6,22 +6,21 @@ function Get-ProjectMetadata { .DESCRIPTION Get metadata for project .EXAMPLE - iex (new-object net.webclient).downloadstring('<%= base_url %>/install.ps1'); Get-ProjectMetadata -project chef -channel stable + iex (new-object net.webclient).downloadstring('<%= base_url || "https://chefdownload-commercial.chef.io" %>/install.ps1<%= base_url && base_url.include?("chefdownload") ? "?license_id=$env:CHEF_LICENSE_KEY" : "" %>'); Get-ProjectMetadata -project chef -channel stable Gets the download url and SHA256 checksum for the latest stable release of Chef. .EXAMPLE - iex (irm '<%= base_url %>/install.ps1'); Get-ProjectMetadata -project chefdk -channel stable -version 0.8.0 - - Gets the download url and SHA256 checksum for ChefDK 0.8.0. + iex (irm '<%= base_url || "https://chefdownload-commercial.chef.io" %>/install.ps1<%= base_url && base_url.include?("chefdownload") ? "?license_id=$env:CHEF_LICENSE_KEY" : "" %>'); Get-ProjectMetadata -project chef-workstation -channel stable #> [cmdletbinding()] param ( # Base url to retrieve metadata from. - [uri]$base_server_uri = '<%= base_url %>', + [uri] + $base_server_uri, [string] # Project to install [string] - $project = 'chef', + $project = '<%= default_product %>', # Version of the application to install # This parameter is optional, if not supplied it will provide the latest version, # and if an iteration number is not specified, it will grab the latest available iteration. @@ -43,7 +42,7 @@ function Get-ProjectMetadata { $architecture = 'auto', # License ID for commercial API access [string] - $license_id + $license_id <%= "= '#{license_id}'" if license_id && !license_id.to_s.empty? %> ) # The following legacy switches are just aliases for the current channel @@ -64,31 +63,47 @@ function Get-ProjectMetadata { Write-Host "Architecture: $architecture" Write-Host "Project: $project" - # Use commercial API if license_id is provided, otherwise use omnitruck - if ($license_id) { - # Check if license_id starts with 'free-' or 'trial-' for trial API - if ($license_id -match '^(free-|trial-)') { - $base_server_uri = 'https://chefdownload-trial.chef.io' - Write-Host "Using Trial API with license ID" - } else { - $base_server_uri = 'https://chefdownload-commercial.chef.io' - Write-Host "Using Commercial API with license ID" - } +<% if base_url && !base_url.to_s.empty? %> + # Set base_server_uri from option if not already set via parameter + if ([string]::IsNullOrEmpty($base_server_uri)) { + $base_server_uri = "<%= base_url %>" } +<% end %> - $metadata_base_url = "/$($channel)/$($project)/metadata" - $metadata_array = ("?v=$($version)", - "p=$platform", - "pv=$platform_version", - "m=$architecture") - - # Add license_id to query parameters if provided - if ($license_id) { - $metadata_array += "license_id=$license_id" + if ([string]::IsNullOrEmpty($base_server_uri)) { + if (-not [string]::IsNullOrEmpty($license_id)) { + # Check if license_id starts with 'free-' or 'trial-' for trial API + if ($license_id -match '^(free-|trial-)') { + # Trial API endpoint + $base_server_uri = "<%= Mixlib::Install::Dist::TRIAL_API_ENDPOINT %>" + } + else { + # Commercial API endpoint + $base_server_uri = "<%= Mixlib::Install::Dist::COMMERCIAL_API_ENDPOINT %>" + } + } + else { + # Omnitruck endpoint + $base_server_uri = "<%= Mixlib::Install::Dist::OMNITRUCK_ENDPOINT %>" + } } - $metadata_base_url += [string]::join('&', $metadata_array) - $metadata_url = new-uri $base_server_uri $metadata_base_url + # For chef-ice product, add platform (p), machine (m), and package_manager (pm) parameters + if (-not [string]::IsNullOrEmpty($license_id)) { + if ($project -eq "chef-ice") { + $package_manager = "msi" + $platform_param = "windows" + $metadata_url = "$base_server_uri$channel/$project/metadata?license_id=$license_id&v=$version&m=$architecture&p=$platform_param&pm=$package_manager" + } + else { + # For other products, use standard parameters + $metadata_url = "$base_server_uri$channel/$project/metadata?license_id=$license_id&v=$version&p=$platform&pv=$platform_version&m=$architecture" + } + } + else { + # Omnitruck URL parameters without license_id + $metadata_url = "$base_server_uri$channel/$project/metadata?v=$version&p=$platform&pv=$platform_version&m=$architecture" + } Write-Host "Downloading $project details from $metadata_url" $response = (Get-WebContent $metadata_url).trim() 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 5e89e155..256624cc 100644 --- a/lib/mixlib/install/generator/powershell/scripts/install_project.ps1.erb +++ b/lib/mixlib/install/generator/powershell/scripts/install_project.ps1.erb @@ -6,12 +6,11 @@ function Install-Project { .DESCRIPTION Install a Chef Software, Inc. product .EXAMPLE - iex (new-object net.webclient).downloadstring('https://omnitruck.chef.io/install.ps1'); Install-Project -project chef -channel stable + iex (new-object net.webclient).downloadstring('<%= base_url || "https://chefdownload-commercial.chef.io" %>/install.ps1<%= base_url && base_url.include?("chefdownload") ? "?license_id=$env:CHEF_LICENSE_KEY" : "" %>'); Install-Project -project chef -channel stable Installs the latest stable version of Chef. .EXAMPLE - iex (irm 'https://omnitruck.chef.io/install.ps1'); Install-Project -project chefdk -channel current - + iex (irm '<<%= base_url || "https://chefdownload-commercial.chef.io" %>/install.ps1<%= base_url && base_url.include?("chefdownload") ? "?license_id=$env:CHEF_LICENSE_KEY" : "" %>'); Install-Project -project chef -channel current Installs the latest integration build of the Chef Development Kit #> [cmdletbinding(SupportsShouldProcess=$true)] @@ -58,6 +57,9 @@ function Install-Project { # Set to 'once' to skip install if project is detected [string] $install_strategy, + # Base server URI for metadata endpoint + [uri] + $base_server_uri, # License ID for commercial API access [string] $license_id <%= "= '#{license_id}'" if license_id && !license_id.to_s.empty? %> @@ -104,7 +106,7 @@ function Install-Project { $download_url = $download_url_override $sha256 = $checksum } else { - $package_metadata = Get-ProjectMetadata -project $project -channel $channel -version $version -prerelease:$prerelease -nightlies:$nightlies -architecture $architecture -license_id $license_id + $package_metadata = Get-ProjectMetadata -project $project -channel $channel -version $version -prerelease:$prerelease -nightlies:$nightlies -architecture $architecture -base_server_uri $base_server_uri -license_id $license_id $download_url = $package_metadata.url $sha256 = $package_metadata.sha256 } @@ -117,12 +119,18 @@ function Install-Project { } } else { - # For licensed downloads, we won't know the filename until after download - if ([string]::IsNullOrEmpty($license_id)) { - $filename = (([System.Uri]$download_url).AbsolutePath -split '/')[-1] - } else { + # Extract filename from URL path (without query params) + $urlPath = (([System.Uri]$download_url).AbsolutePath -split '/')[-1] + + # Check if URL path has a package file extension + $hasPackageExtension = $urlPath -match '\.(msi|appx|pkg|dmg|rpm|deb)$' + + # For licensed downloads or URLs without package extensions, we won't know the filename until after download + if (-not [string]::IsNullOrEmpty($license_id) -or -not $hasPackageExtension) { $filename = "chef-download-temp-$PID" - Write-Host "Using temporary filename for licensed download: $filename" + Write-Host "Using temporary filename for content-disposition download: $filename" + } else { + $filename = $urlPath } } Write-Host "Download directory: $download_directory" @@ -163,8 +171,8 @@ function Install-Project { Write-Host "Downloading $project from $($download_url) to $download_destination." $download_result = Get-WebContent $download_url -filepath $download_destination - # For licensed downloads, extract actual filename from Content-Disposition - if (-not [string]::IsNullOrEmpty($license_id) -and $download_result -and $download_result.Filename) { + # Extract actual filename from Content-Disposition if it was a temp filename + if ($filename -like "chef-download-temp-*" -and $download_result -and $download_result.Filename) { $actual_filename = $download_result.Filename Write-Host "Extracted filename from Content-Disposition: $actual_filename" @@ -179,8 +187,8 @@ function Install-Project { move-item $download_destination $final_destination -force $download_destination = $final_destination } - } elseif (-not [string]::IsNullOrEmpty($license_id)) { - Write-Host "Warning: Could not extract filename from Content-Disposition header for licensed download." + } elseif ($filename -like "chef-download-temp-*") { + Write-Host "Warning: Could not extract filename from Content-Disposition header." Write-Host "Using temporary filename. Package installation may fail." } } @@ -194,6 +202,7 @@ function Install-Project { Write-Host "Installing $project from $download_destination" $installingProject = $True $installAttempts = 0 + $maxAttempts = 5 while ($installingProject) { $installAttempts++ $result = $false @@ -203,8 +212,15 @@ function Install-Project { else { $result = Install-ChefMsi $download_destination $daemon } - if(!$result) { continue } + if(!$result) { + if($installAttempts -ge $maxAttempts) { + Write-Host "Failed to install $project after $installAttempts attempts." + throw "Installation failed after $installAttempts attempts." + } + continue + } $installingProject = $False + Write-Host "$project installation completed successfully." } } } diff --git a/lib/mixlib/install/options.rb b/lib/mixlib/install/options.rb index e7694bf2..4760065b 100644 --- a/lib/mixlib/install/options.rb +++ b/lib/mixlib/install/options.rb @@ -64,6 +64,7 @@ class InvalidOptions < ArgumentError; end :user_agent_headers, :install_command_options, :license_id, + :base_url, ] SUPPORTED_WINDOWS_DESKTOP_VERSIONS = %w{10} diff --git a/lib/mixlib/install/product_matrix.rb b/lib/mixlib/install/product_matrix.rb index 0a692e63..0c61e6a5 100644 --- a/lib/mixlib/install/product_matrix.rb +++ b/lib/mixlib/install/product_matrix.rb @@ -132,7 +132,7 @@ end config_file "/etc/delivery/delivery.rb" github_repo "chef/automate" - downloads_product_page_url "https://downloads.chef.io/automate" + downloads_product_page_url "#{Mixlib::Install::Dist::DOWNLOADS_PAGE}/automate" end product "ha" do diff --git a/lib/mixlib/install/script_generator.rb b/lib/mixlib/install/script_generator.rb index c222f91c..e6fe31a7 100644 --- a/lib/mixlib/install/script_generator.rb +++ b/lib/mixlib/install/script_generator.rb @@ -58,6 +58,7 @@ def sudo_command=(cmd) attr_accessor :install_msi_url attr_accessor :license_id + attr_accessor :base_url VALID_INSTALL_OPTS = %w{omnibus_url endpoint @@ -71,7 +72,8 @@ def sudo_command=(cmd) project root use_sudo - sudo_command} + sudo_command + base_url} def initialize(version, powershell = false, opts = {}) @version = (version || "latest").to_s.downcase @@ -82,7 +84,7 @@ def initialize(version, powershell = false, opts = {}) @prerelease = false @nightlies = false @endpoint = "metadata" - @omnibus_url = "https://omnitruck.chef.io/install.sh" + @omnibus_url = "#{Mixlib::Install::Dist::OMNITRUCK_ENDPOINT}/install.sh" @use_sudo = true @sudo_command = "sudo -E" @license_id = nil @@ -96,6 +98,15 @@ def initialize(version, powershell = false, opts = {}) end 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 end def install_command @@ -213,7 +224,7 @@ def shell_var(name, value) # @return the correct Chef Omnitruck API metadata endpoint, based on project def metadata_endpoint_from_project(project = nil) - if project.nil? || project.casecmp("chef") == 0 + if project.nil? || project.casecmp(Mixlib::Install::Dist::DEFAULT_PRODUCT) == 0 "metadata" else "metadata-#{project.downcase}" @@ -224,16 +235,19 @@ def metadata_endpoint_from_project(project = nil) # @return [String] the omnibus URL (commercial/trial or standard omnitruck) # @api private def omnibus_url_for_license - return omnibus_url if license_id.nil? || license_id.to_s.empty? || omnibus_url != "https://omnitruck.chef.io/install.sh" - - # Determine if this is a trial or commercial license - base_url = if license_id.start_with?("free-", "trial-") - "https://chefdownload-trial.chef.io" - else - "https://chefdownload-commercial.chef.io" - end - - "#{base_url}/install.sh?license_id=#{CGI.escape(license_id)}" + return omnibus_url if license_id.nil? || license_id.to_s.empty? || omnibus_url != "#{Mixlib::Install::Dist::OMNITRUCK_ENDPOINT}/install.sh" + + # Use custom base_url if provided, otherwise determine from license type + endpoint_base = if @base_url + @base_url + elsif license_id.start_with?("free-", "trial-") + Mixlib::Install::Dist::TRIAL_API_ENDPOINT + else + Mixlib::Install::Dist::COMMERCIAL_API_ENDPOINT + end + + # Add license_id as query param when using licensed endpoints + "#{endpoint_base}/install.sh?license_id=#{CGI.escape(license_id)}" end def windows_metadata_url @@ -242,14 +256,17 @@ def windows_metadata_url if using_licensed_api # Commercial/trial API: ///metadata - base_url = if license_id.start_with?("free-", "trial-") - "https://chefdownload-trial.chef.io" - else - "https://chefdownload-commercial.chef.io" - end + # Use custom base_url if provided, otherwise determine from license type + endpoint_base = if @base_url + @base_url + elsif license_id.start_with?("free-", "trial-") + Mixlib::Install::Dist::TRIAL_API_ENDPOINT + else + Mixlib::Install::Dist::COMMERCIAL_API_ENDPOINT + end product_name = @project - url = "#{base_url}/#{@channel}/#{product_name}/metadata" + url = "#{endpoint_base}/#{@channel}/#{product_name}/metadata" else # Omnitruck API: use base from omnibus_url + endpoint base = if omnibus_url_for_license.match?(%r{/install.sh}) @@ -261,7 +278,14 @@ def windows_metadata_url url = "#{base}#{endpoint}" end - url << "?p=windows&m=$platform_architecture&pv=$platform_version" + # chef-ice uses different parameters than chef + if @project.casecmp("chef-ice") == 0 + # For chef-ice: p (platform), m (machine), pm (package_manager) + url << "?p=windows&m=$platform_architecture&pm=msi" + else + # For chef and other products: p (platform), pv (platform_version), m (machine) + url << "?p=windows&m=$platform_architecture&pv=$platform_version" + end url << "&v=#{CGI.escape(version)}" unless %w{latest true nightlies}.include?(version) url << "&prerelease=true" if prerelease url << "&nightlies=true" if nightlies diff --git a/lib/mixlib/install/util.rb b/lib/mixlib/install/util.rb index e299e34d..1cbdb603 100644 --- a/lib/mixlib/install/util.rb +++ b/lib/mixlib/install/util.rb @@ -172,6 +172,53 @@ def normalize_architecture(architecture) architecture end end + + # + # Determines package manager based on platform for commercial API + # + # @param [String] platform + # + # @return String [package_manager] (rpm, deb, tar, msi, dmg) + def determine_package_manager(platform) + case platform + when /^el/, /^centos/, /^rhel/, /^fedora/, /^amazon/, /^rocky/, /^opensuse/, /^sles/, /^scientific/ + "rpm" + when /^debian/, /^ubuntu/, /^linuxmint/, /^raspbian/ + "deb" + when /^mac_os_x/, /^macos/ + "dmg" + when /^windows/ + "msi" + when /^solaris/, /^smartos/, /^freebsd/, /^aix/, /^omnios/ + "tar" + else + # Default to tar for unknown platforms + "tar" + end + end + + # + # Normalizes platform name for commercial API (chef-ice) + # Maps specific platform names to generic categories + # + # @param [String] platform + # + # @return String [normalized_platform] (linux, macos, windows) + def normalize_platform_for_commercial(platform) + case platform + when /^el/, /^centos/, /^rhel/, /^fedora/, /^rocky/, /^scientific/, /^debian/, /^ubuntu/, /^linuxmint/, /^raspbian/, /^opensuse/, /^sles/, /^amazon/ + "linux" + when /^mac_os_x/, /^macos/ + "macos" + when /^windows/ + "windows" + when /^freebsd/, /^aix/, /^solaris/, /^smartos/, /^omnios/ + "unix" + else + # Default to linux for unknown platforms + "linux" + end + end end end end diff --git a/spec/unit/mixlib/install/backend/package_router_spec.rb b/spec/unit/mixlib/install/backend/package_router_spec.rb index 32616e39..21f5b055 100644 --- a/spec/unit/mixlib/install/backend/package_router_spec.rb +++ b/spec/unit/mixlib/install/backend/package_router_spec.rb @@ -202,6 +202,186 @@ end end + context "for chef-ice with commercial API" do + let(:channel) { :current } + let(:product_name) { "chef-ice" } + let(:product_version) { "19.1.151" } + let(:license_id) { "test-license-key-123" } + let(:platform) { "ubuntu" } + let(:platform_version) { "20.04" } + let(:architecture) { "x86_64" } + + before do + # Mock the HTTP request to prevent actual API calls + # chef-ice API structure: platform -> architecture -> package_manager -> package_info + allow(package_router).to receive(:get).and_return({ + "linux" => { + "x86_64" => { + "deb" => { + "version" => "19.1.151", + "sha256" => "abc123def456", + "sha1" => "ghi789", + }, + }, + }, + }) + end + + it "uses commercial API endpoint" do + expect(package_router.endpoint).to eq Mixlib::Install::Dist::COMMERCIAL_API_ENDPOINT + end + + it "constructs download URL with normalized platform parameter" do + artifact = artifact_info + expect(artifact.url).to include("p=linux") + end + + it "constructs download URL with package manager parameter" do + artifact = artifact_info + expect(artifact.url).to include("pm=deb") + end + + it "constructs download URL with machine architecture parameter" do + artifact = artifact_info + expect(artifact.url).to include("m=x86_64") + end + + it "constructs download URL with license_id parameter" do + artifact = artifact_info + expect(artifact.url).to include("license_id=#{license_id}") + end + + it "constructs complete chef-ice download URL" do + artifact = artifact_info + expect(artifact.url).to match(%r{/current/chef-ice/download\?v=19\.1\.151&license_id=#{license_id}&m=x86_64&p=linux&pm=deb}) + end + + context "on RPM-based platform" do + let(:platform) { "el" } + let(:platform_version) { "8" } + + before do + # chef-ice API structure: platform -> architecture -> package_manager -> package_info + allow(package_router).to receive(:get).and_return({ + "linux" => { + "x86_64" => { + "rpm" => { + "version" => "19.1.151", + "sha256" => "abc123def456", + "sha1" => "ghi789", + }, + }, + }, + }) + end + + it "uses rpm package manager" do + artifact = artifact_info + expect(artifact.url).to include("pm=rpm") + end + end + + context "on macOS platform" do + let(:platform) { "mac_os_x" } + let(:platform_version) { "12" } + + before do + # chef-ice API structure: platform -> architecture -> package_manager -> package_info + allow(package_router).to receive(:get).and_return({ + "macos" => { + "x86_64" => { + "dmg" => { + "version" => "19.1.151", + "sha256" => "abc123def456", + "sha1" => "ghi789", + }, + }, + }, + }) + end + + it "uses macos platform parameter" do + artifact = artifact_info + expect(artifact.url).to include("p=macos") + end + + it "uses dmg package manager" do + artifact = artifact_info + expect(artifact.url).to include("pm=dmg") + end + end + end + + context "for chef-ice with trial API" do + let(:channel) { :current } + let(:product_name) { "chef-ice" } + let(:product_version) { "19.1.151" } + let(:license_id) { "free-trial-xyz-123" } + let(:platform) { "ubuntu" } + let(:platform_version) { "20.04" } + let(:architecture) { "x86_64" } + + before do + # Mock the HTTP request to prevent actual API calls + # chef-ice API structure: platform -> architecture -> package_manager -> package_info + allow(package_router).to receive(:get).and_return({ + "linux" => { + "x86_64" => { + "deb" => { + "version" => "19.1.151", + "sha256" => "abc123def456", + "sha1" => "ghi789", + }, + }, + }, + }) + end + + it "uses trial API endpoint" do + expect(package_router.endpoint).to eq Mixlib::Install::Dist::TRIAL_API_ENDPOINT + end + + it "constructs download URL with normalized platform parameter" do + artifact = artifact_info + expect(artifact.url).to include("p=linux") + end + + it "constructs download URL with package manager parameter" do + artifact = artifact_info + expect(artifact.url).to include("pm=deb") + end + + it "constructs download URL with machine architecture parameter" do + artifact = artifact_info + expect(artifact.url).to include("m=x86_64") + end + + it "constructs download URL with license_id parameter" do + artifact = artifact_info + expect(artifact.url).to include("license_id=#{license_id}") + end + + it "constructs complete chef-ice download URL with trial endpoint" do + artifact = artifact_info + # Trial API automatically defaults to stable channel and latest version + expect(artifact.url).to match(%r{https://chefdownload-trial\.chef\.io/stable/chef-ice/download\?v=19\.1\.151&license_id=#{license_id}&m=x86_64&p=linux&pm=deb}) + end + + context "with trial- prefix" do + let(:license_id) { "trial-abc-456" } + + it "uses trial API endpoint" do + expect(package_router.endpoint).to eq Mixlib::Install::Dist::TRIAL_API_ENDPOINT + end + + it "constructs complete chef-ice download URL with trial endpoint" do + artifact = artifact_info + # Trial API automatically defaults to stable channel and latest version + expect(artifact.url).to match(%r{https://chefdownload-trial\.chef\.io/stable/chef-ice/download\?v=19\.1\.151&license_id=#{license_id}&m=x86_64&p=linux&pm=deb}) + end + end + end + context "for chef/stable with specific version" do let(:channel) { :stable } let(:product_name) { "chef" } diff --git a/spec/unit/mixlib/install/generator/base_spec.rb b/spec/unit/mixlib/install/generator/base_spec.rb index 74dda10b..2c434353 100644 --- a/spec/unit/mixlib/install/generator/base_spec.rb +++ b/spec/unit/mixlib/install/generator/base_spec.rb @@ -86,28 +86,32 @@ def self.script_base_path script = test_generator_class.get_script("test_script.sh", {}) expect(script).to include("project=Chef") - expect(script).to include("url=https://omnitruck.chef.io") + # base_url should be empty when not provided - scripts determine URL at runtime + expect(script).to include("url=\n") end - it "sets base_url to commercial API when license_id is provided" do + it "does not set base_url from license_id alone" do context = { license_id: "test-commercial-key" } script = test_generator_class.get_script("test_script.sh", context) - expect(script).to include("url=https://chefdownload-commercial.chef.io") + # base_url should be empty - license_id doesn't auto-set it in context + expect(script).to include("url=\n") end - it "sets base_url to trial API when free- license_id is provided" do + it "does not set base_url from free- license_id alone" do context = { license_id: "free-trial-123" } script = test_generator_class.get_script("test_script.sh", context) - expect(script).to include("url=https://chefdownload-trial.chef.io") + # base_url should be empty - license_id doesn't auto-set it in context + expect(script).to include("url=\n") end - it "sets base_url to trial API when trial- license_id is provided" do + it "does not set base_url from trial- license_id alone" do context = { license_id: "trial-xyz-456" } script = test_generator_class.get_script("test_script.sh", context) - expect(script).to include("url=https://chefdownload-trial.chef.io") + # base_url should be empty - license_id doesn't auto-set it in context + expect(script).to include("url=\n") end end @@ -165,6 +169,13 @@ def self.script_base_path FileUtils.rm_rf(@temp_dir) if @temp_dir end + it "uses habitat directory for chef-ice" do + context = { default_product: "chef-ice" } + script = test_generator_class.get_script("windows_dir.sh", context) + + expect(script).to include("dir=hab\\pkgs") + end + it "uses omnibus directory for chef" do context = { default_product: "chef" } script = test_generator_class.get_script("windows_dir.sh", context) @@ -239,7 +250,8 @@ def self.script_base_path script = test_generator_class.get_script("defaults.sh", {}) expect(script).to include("project=Chef") - expect(script).to include("url=https://omnitruck.chef.io") + # base_url should be empty when not provided - scripts determine URL at runtime + expect(script).to include("url=\n") expect(script).to include("product=chef") expect(script).to include("bug=https://github.com/chef/omnitruck/issues/new") expect(script).to include("support=https://www.chef.io/support/tickets") diff --git a/spec/unit/mixlib/install/generator_spec.rb b/spec/unit/mixlib/install/generator_spec.rb index 77db4253..aa436a05 100644 --- a/spec/unit/mixlib/install/generator_spec.rb +++ b/spec/unit/mixlib/install/generator_spec.rb @@ -229,6 +229,54 @@ end end + context "with base_url" do + let(:add_options) do + { + base_url: "https://custom.chef.io", + } + end + + it "includes base_url in the script" do + expect(install_script).to include("base_api_url=\"https://custom.chef.io\"") + end + + it "uses custom base_url in metadata fetch" do + expect(install_script).to include("https://custom.chef.io") + end + + it "uses traditional text parsing without license_id" do + expect(install_script).to include("awk '$1 == \"url\" { print $2 }'") + expect(install_script).to include("grep '^url' $metadata_filename") + end + end + + context "with base_url and license_id" do + let(:add_options) do + { + base_url: "https://custom.chef.io", + license_id: "test-license-123", + } + end + + it "includes both base_url and license_id in the script" do + expect(install_script).to include("base_api_url=\"https://custom.chef.io\"") + expect(install_script).to include("license_id=test-license-123") + end + + it "uses custom base_url even with license_id" do + expect(install_script).to include("https://custom.chef.io") + # The script should set base_api_url to the custom URL in the conditional block + expect(install_script).to match(/if \[ -z "\$base_api_url" \]; then\s+base_api_url="https:\/\/custom\.chef\.io"/m) + # Verify the script includes the base_api_url variable assignment with custom URL + expect(install_script).to include('base_api_url="https://custom.chef.io"') + end + + it "includes JSON parsing logic for commercial API" do + expect(install_script).to include("sed -n 's/.*\"url\":\"\\([^\"]*\\)\".*/\\1/p'") + expect(install_script).to include("sed -n 's/.*\"sha256\":\"\\([^\"]*\\)\".*/\\1/p'") + end + end + context "filename extraction for content-disposition" do let(:add_options) do { @@ -254,6 +302,85 @@ end end + context "chef-ice with commercial API" do + let(:add_options) do + { + license_id: "test-license-key-123", + } + end + + it "includes package manager detection function" do + expect(install_script).to include("determine_package_manager()") + end + + it "includes platform normalization function" do + expect(install_script).to include("normalize_platform_name()") + end + + it "includes chef-ice conditional logic" do + expect(install_script).to include('if [ "$project" = "chef-ice" ]; then') + end + + it "includes RPM-based platform detection" do + expect(install_script).to include("el|centos|rhel|fedora|amazon|rocky") + expect(install_script).to include('echo "rpm"') + end + + it "includes DEB-based platform detection" do + expect(install_script).to include("debian|ubuntu|linuxmint|raspbian") + expect(install_script).to include('echo "deb"') + end + + it "includes TAR-based platform detection" do + expect(install_script).to include("mac_os_x|macos|solaris*|smartos|freebsd|aix") + expect(install_script).to include('echo "tar"') + end + + it "includes platform normalization for Linux" do + expect(install_script).to include("el|centos|rhel|fedora|rocky") + expect(install_script).to include('echo "linux"') + end + + it "includes platform normalization for macOS" do + expect(install_script).to include("mac_os_x|macos") + expect(install_script).to include('echo "macos"') + end + + it "constructs chef-ice metadata URL with m, p, pm parameters" do + expect(install_script).to include('metadata_url="$base_api_url/$channel/$project/metadata?license_id=$license_id&v=$version&m=$machine&p=$platform_param&pm=$package_manager"') + end + + it "uses commercial API endpoint" do + expect(install_script).to include("https://chefdownload-commercial.chef.io") + end + end + + context "chef-ice with trial API" do + let(:add_options) do + { + license_id: "free-trial-xyz-123", + } + end + + it "includes chef-ice conditional logic" do + expect(install_script).to include('if [ "$project" = "chef-ice" ]; then') + end + + it "constructs chef-ice metadata URL with m, p, pm parameters" do + expect(install_script).to include('metadata_url="$base_api_url/$channel/$project/metadata?license_id=$license_id&v=$version&m=$machine&p=$platform_param&pm=$package_manager"') + end + + it "uses trial API endpoint" do + expect(install_script).to include("https://chefdownload-trial.chef.io") + end + + it "works with trial- prefix" do + add_options[:license_id] = "trial-abc-456" + expect(install_script).to include("https://chefdownload-trial.chef.io") + expect(install_script).to include('if [ "$project" = "chef-ice" ]; then') + end + end + context "for windows" do shared_examples_for "the correct ps1 script" do it "generates a ps1 script" do @@ -382,6 +509,118 @@ expect(install_script).to include("$json.sha256") end end + + context "with base_url for PowerShell" do + let(:add_options) do + { + shell_type: :ps1, + base_url: "https://custom.chef.io", + } + end + + it_behaves_like "the correct ps1 script" + + it "includes base_url in the script" do + expect(install_script).to include('$base_server_uri = "https://custom.chef.io"') + end + + it "uses custom base_url in metadata fetch" do + expect(install_script).to include("https://custom.chef.io") + end + + it "uses traditional text parsing without license_id" do + expect(install_script).to include("-split '\\n'") + expect(install_script).to include("$key, $value = $_ -split '\\s+'") + end + end + + context "with base_url and license_id for PowerShell" do + let(:add_options) do + { + shell_type: :ps1, + base_url: "https://custom.chef.io", + license_id: "test-license-123", + } + end + + it_behaves_like "the correct ps1 script" + + it "includes both base_url and license_id in the script" do + expect(install_script).to include('$base_server_uri = "https://custom.chef.io"') + expect(install_script).to include("test-license-123") + end + + it "uses custom base_url even with license_id" do + expect(install_script).to include("https://custom.chef.io") + # Script should conditionally assign base_server_uri, not hardcode commercial endpoint + expect(install_script).to match(/\$base_server_uri\s*=.*if.*else/m) + end + + it "includes JSON parsing logic for commercial API" do + expect(install_script).to include("ConvertFrom-Json") + expect(install_script).to include("$json.url") + expect(install_script).to include("$json.sha256") + end + end + + context "chef-ice with commercial API for PowerShell" 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 "includes chef-ice conditional logic" do + expect(install_script).to include('if ($project -eq "chef-ice")') + end + + it "includes simplified parameters for chef-ice on Windows" do + expect(install_script).to include('$platform_param = "windows"') + expect(install_script).to include('$package_manager = "msi"') + end + + it "constructs chef-ice metadata URL with m, p, pm parameters" do + expect(install_script).to include('$metadata_url = "$base_server_uri$channel/$project/metadata?license_id=$license_id&v=$version&m=$architecture&p=$platform_param&pm=$package_manager"') + end + + it "uses commercial API endpoint" do + expect(install_script).to include("https://chefdownload-commercial.chef.io") + end + end + + context "chef-ice with trial API for PowerShell" do + let(:add_options) do + { + product_name: "chef-ice", + shell_type: :ps1, + license_id: "free-trial-xyz-123", + } + end + + it_behaves_like "the correct ps1 script" + + it "includes chef-ice conditional logic" do + expect(install_script).to include('if ($project -eq "chef-ice")') + end + + it "includes simplified parameters for chef-ice on Windows" do + expect(install_script).to include('$platform_param = "windows"') + expect(install_script).to include('$package_manager = "msi"') + end + + it "uses trial API endpoint" do + expect(install_script).to include("https://chefdownload-trial.chef.io") + end + + it "works with trial- prefix" do + add_options[:license_id] = "trial-abc-456" + expect(install_script).to include("https://chefdownload-trial.chef.io") + end + end end end diff --git a/spec/unit/mixlib/install/options_spec.rb b/spec/unit/mixlib/install/options_spec.rb index 9dcdc3bf..4afea645 100644 --- a/spec/unit/mixlib/install/options_spec.rb +++ b/spec/unit/mixlib/install/options_spec.rb @@ -159,6 +159,22 @@ end end + context "for base_url option" do + let(:product_name) { "chef" } + let(:channel) { :stable } + let(:base_url) { "https://custom.chef.io" } + + it "accepts base_url parameter" do + mi = Mixlib::Install.new(product_name: product_name, channel: channel, base_url: base_url) + expect(mi.options.base_url).to eq base_url + end + + it "allows nil base_url" do + mi = Mixlib::Install.new(product_name: product_name, channel: channel) + expect(mi.options.base_url).to be_nil + end + end + context "for trial API defaults" do let(:product_name) { "chef" } diff --git a/spec/unit/mixlib/install/script_generator_spec.rb b/spec/unit/mixlib/install/script_generator_spec.rb index 692e297d..187368e5 100644 --- a/spec/unit/mixlib/install/script_generator_spec.rb +++ b/spec/unit/mixlib/install/script_generator_spec.rb @@ -52,6 +52,18 @@ install = described_class.new("1.2.1", false, root: "/opt/test") expect(install.root).to eq("/opt/test") end + + describe "for chef-ice product" do + it "uses Habitat install directory on windows" do + install = described_class.new("1.2.1", true, project: "chef-ice") + expect(install.root).to eq("$env:systemdrive\\hab\\pkgs\\chef\\chef-infra-client\\*\\*") + end + + it "uses Habitat install directory on unix" do + install = described_class.new("1.2.1", false, project: "chef-ice") + expect(install.root).to eq("/hab/pkgs/chef/chef-infra-client/*/*") + end + end end describe "parses the options hash" do @@ -84,6 +96,12 @@ install = described_class.new("1.2.1", false, opts) expect(install.license_id).to eq("test-license-123") end + + it "sets the base_url" do + opts = { base_url: "https://custom.chef.io" } + install = described_class.new("1.2.1", false, opts) + expect(install.base_url).to eq("https://custom.chef.io") + end end end @@ -167,6 +185,27 @@ end end + describe "with chef-ice product" do + let(:installer) { described_class.new("latest", true, omnibus_url: "http://f/install.sh", project: "chef-ice", license_id: "test-ice-123") } + + it "uses stable/chef-ice/metadata endpoint" do + metadata_url = installer.send(:windows_metadata_url) + expect(metadata_url).to include("stable/chef-ice/metadata") + end + + it "includes pm parameter instead of pv for chef-ice" do + metadata_url = installer.send(:windows_metadata_url) + expect(metadata_url).to include("pm=msi") + expect(metadata_url).not_to include("pv=") + end + + it "includes p and m parameters" do + metadata_url = installer.send(:windows_metadata_url) + expect(metadata_url).to include("p=windows") + expect(metadata_url).to include("m=$platform_architecture") + end + end + describe "with chef product" do let(:installer) { described_class.new("latest", true, omnibus_url: "http://f/install.sh", project: "chef", license_id: "test-chef-123") } @@ -235,6 +274,15 @@ end end + describe "with chef-ice product" do + let(:installer) { described_class.new("latest", false, omnibus_url: "https://omnitruck.chef.io/install.sh", project: "chef-ice", license_id: "test-ice-456") } + + it "uses commercial URL for chef-ice" do + out = installer.install_command + expect(out).to include('chef_omnibus_url="https://chefdownload-commercial.chef.io/install.sh?license_id=test-ice-456"') + end + end + describe "with custom omnibus_url and license" do let(:installer) { described_class.new("latest", false, omnibus_url: "https://custom.example.com/install.sh", license_id: "test-123") } @@ -314,6 +362,30 @@ expect(installer.send(:omnibus_url_for_license)).to eq("https://example.com/custom_path") end end + + context "with base_url and commercial license_id" do + before do + installer.license_id = "commercial-abc123" + installer.base_url = "https://custom.chef.io" + end + + it "returns custom base_url with license_id" do + url = installer.send(:omnibus_url_for_license) + expect(url).to eq("https://custom.chef.io/install.sh?license_id=commercial-abc123") + end + end + + context "with base_url and trial license_id" do + before do + installer.license_id = "trial-xyz789" + installer.base_url = "https://custom.chef.io" + end + + it "returns custom base_url with license_id" do + url = installer.send(:omnibus_url_for_license) + expect(url).to eq("https://custom.chef.io/install.sh?license_id=trial-xyz789") + end + end end describe "#windows_metadata_url" do @@ -343,6 +415,26 @@ expect(url).not_to include("&v=") end end + + context "with base_url and commercial license" do + let(:installer) { described_class.new("16.0.0", true, omnibus_url: "https://omnitruck.chef.io/install.sh", license_id: "commercial-123", base_url: "https://custom.chef.io") } + + it "uses custom base_url in metadata URL" do + url = installer.send(:windows_metadata_url) + expect(url).to include("https://custom.chef.io/stable/chef/metadata") + expect(url).to include("license_id=commercial-123") + end + end + + context "with base_url and trial license" do + let(:installer) { described_class.new("16.0.0", true, omnibus_url: "https://omnitruck.chef.io/install.sh", license_id: "trial-456", base_url: "https://custom.chef.io") } + + it "uses custom base_url in metadata URL" do + url = installer.send(:windows_metadata_url) + expect(url).to include("https://custom.chef.io/stable/chef/metadata") + expect(url).to include("license_id=trial-456") + end + end end end end diff --git a/spec/unit/mixlib/install/util_spec.rb b/spec/unit/mixlib/install/util_spec.rb index d04b271a..235359fb 100644 --- a/spec/unit/mixlib/install/util_spec.rb +++ b/spec/unit/mixlib/install/util_spec.rb @@ -147,4 +147,88 @@ end end end + + describe ".determine_package_manager" do + context "RPM-based platforms" do + %w{el centos rhel fedora amazon rocky opensuse sles scientific}.each do |platform| + it "returns rpm for #{platform}" do + expect(Mixlib::Install::Util.determine_package_manager(platform)).to eq "rpm" + end + end + end + + context "DEB-based platforms" do + %w{debian ubuntu linuxmint raspbian}.each do |platform| + it "returns deb for #{platform}" do + expect(Mixlib::Install::Util.determine_package_manager(platform)).to eq "deb" + end + end + end + + context "macOS platforms" do + %w{mac_os_x macos}.each do |platform| + it "returns dmg for #{platform}" do + expect(Mixlib::Install::Util.determine_package_manager(platform)).to eq "dmg" + end + end + end + + context "Windows platform" do + it "returns msi for windows" do + expect(Mixlib::Install::Util.determine_package_manager("windows")).to eq "msi" + end + end + + context "TAR-based platforms" do + %w{solaris smartos freebsd aix omnios}.each do |platform| + it "returns tar for #{platform}" do + expect(Mixlib::Install::Util.determine_package_manager(platform)).to eq "tar" + end + end + end + + context "unknown platform" do + it "returns tar as default" do + expect(Mixlib::Install::Util.determine_package_manager("unknown")).to eq "tar" + end + end + end + + describe ".normalize_platform_for_commercial" do + context "Linux platforms" do + %w{el centos rhel fedora rocky scientific debian ubuntu linuxmint raspbian opensuse sles amazon}.each do |platform| + it "returns linux for #{platform}" do + expect(Mixlib::Install::Util.normalize_platform_for_commercial(platform)).to eq "linux" + end + end + end + + context "macOS platforms" do + %w{mac_os_x macos}.each do |platform| + it "returns macos for #{platform}" do + expect(Mixlib::Install::Util.normalize_platform_for_commercial(platform)).to eq "macos" + end + end + end + + context "Windows platform" do + it "returns windows for windows" do + expect(Mixlib::Install::Util.normalize_platform_for_commercial("windows")).to eq "windows" + end + end + + context "Unix platforms" do + %w{freebsd aix solaris smartos omnios}.each do |platform| + it "returns unix for #{platform}" do + expect(Mixlib::Install::Util.normalize_platform_for_commercial(platform)).to eq "unix" + end + end + end + + context "unknown platform" do + it "returns linux as default" do + expect(Mixlib::Install::Util.normalize_platform_for_commercial("unknown")).to eq "linux" + end + end + end end diff --git a/spec/unit/mixlib/install_spec.rb b/spec/unit/mixlib/install_spec.rb index b34ff53a..dc3d1b90 100644 --- a/spec/unit/mixlib/install_spec.rb +++ b/spec/unit/mixlib/install_spec.rb @@ -75,6 +75,28 @@ expect(installer.current_version).to eq(nil) end end + + context "with chef-ice product" do + let(:product_name) { "chef-ice" } + let(:version_manifest_file) { "/hab/pkgs/chef/chef-infra-client/*/*/version-manifest.json" } + + it "should use Habitat install directory path" do + expect(installer.root).to eq("/hab/pkgs/chef/chef-infra-client/*/*") + end + + context "when chef-ice 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 diff --git a/support/install_command.ps1 b/support/install_command.ps1 index df1e9ba0..32e296dd 100644 --- a/support/install_command.ps1 +++ b/support/install_command.ps1 @@ -1,5 +1,6 @@ # Set strict error handling to ensure errors cause script failures $ErrorActionPreference = 'Stop' + Function Check-UpdateChef($root, $version) { if (-Not (Test-Path "$root\embedded")) { return $true } elseif ("$version" -eq "true") { return $false }