Skip to content

CHEF-34005: Auto-configure Chef Premium RubyGem server as gem source - #349

Open
sanghinitin wants to merge 1 commit into
mainfrom
CHEF-34005-chef-ruby-server
Open

CHEF-34005: Auto-configure Chef Premium RubyGem server as gem source#349
sanghinitin wants to merge 1 commit into
mainfrom
CHEF-34005-chef-ruby-server

Conversation

@sanghinitin

Copy link
Copy Markdown
Contributor

Summary

Automatically configure the Chef Premium RubyGem server (rubygems.chef.io) as a gem source before any chef gem install, search, fetch, update, or download operation, so users can install premium extensions (knife plugins, kitchen drivers) without manual setup.

Problem

Premium Chef extensions are distributed via rubygems.chef.io, which requires authentication via a license key. Previously, users had to manually run gem source --add https://v1:\<key\>@rubygems.chef.io before chef gem install would work.

Solution

The chef gem command now checks the configured gem sources before any remote-fetching subcommand and takes the appropriate action:

USER RUNS: chef gem install <gem>
          |
          v
Is rubygems.chef.io already configured?
  YES → proceed with install
  NO  →
    Is there a custom source that is NOT rubygems.org and NOT rubygems.chef.io?
      YES → assume air-gapped mirror, warn, skip (do not modify sources)
      NO  →
        Fetch license key via ChefLicensing (ENV → --chef-license-key arg → terminal prompt)
          FOUND  → run `gem sources --add https://v1:<key>@rubygems.chef.io` then proceed
          NOT FOUND → warn user to run `chef license add`, proceed with rubygems.org only

Files Changed

File Change
lib/chef-cli/command/gem.rb Added ensure_chef_gem_source, premium_source_command?, configured_source_hosts, add_chef_gem_source, chef_license_key
spec/unit/command/gem_spec.rb 15 new examples covering all acceptance criteria

Test Evidence

bundle exec rspec spec/unit/command/gem_spec.rb --format documentation

ChefCLI::Command::GemForwarder
  #run
    when NOT in a Habitat environment
      calls ensure_chef_gem_source before forwarding to GemRunner
      forwards params to Gem::GemRunner
      does not modify GEM_HOME
      returns true when GemRunner returns nil
    when in a Habitat environment
      sets GEM_HOME to user gem directory
      sets GEM_PATH to include both user gem dir and existing GEM_PATH
      clears Gem paths after setting environment
      creates the gem directory if it doesn't exist
      does not create the gem directory if it already exists
      forwards all gem subcommands correctly
    when CHEF_GEM_HOME_ENABLED is set but habitat_install? is false
      still sets up gem environment via env var detection
    when GemRunner raises Gem::SystemExitException
      exits with the exception's exit code
  #ensure_chef_gem_source
    when the command does not download from remote sources
      does nothing for non-install/search commands
    when the Chef Premium RubyGem source is already configured
      does not attempt to add it again
    when only the default RubyGems.org source is configured
      adds the Chef source built from the license key
      uses fetch_and_persist to retrieve the license key (ENV→arg→terminal priority)
      also triggers for the search command
    when only a custom source is configured (no rubygems.org)
      assumes air-gapped and does not add the Chef source
      warns about air-gapped environment
    when a custom non-chef, non-rubygems source is configured
      assumes an air-gapped mirror and does not add the Chef source
      warns the user about the air-gapped assumption
    when no license key can be obtained
      does not add a source and warns about premium extensions
      does not raise an error so the install proceeds against rubygems.org
    when fetching the license key raises an error
      treats it as no license key and does not add a source
  #add_chef_gem_source
    invokes gem sources --add with the premium URL
    does not propagate a Gem::SystemExitException so the user command still runs

31 examples, 0 failures
Line Coverage: 82.64% (100 / 121)

Acceptance Criteria Coverage

TC Description Status
TC-01 Chef source already configured → no modification
TC-02/03/04 Only rubygems.org + license key (ENV/arg/terminal) → adds source
TC-05 No license key → warn, install continues gracefully
TC-06 Only custom source (no rubygems.org) → airgap, skip
TC-07 rubygems.org + custom source → airgap, skip
TC-08/09/10 Habitat GEM_HOME/GEM_PATH persistence
TC-11 Non-Habitat mode: source check still runs
TC-13 fetch_and_persist called (preserves ENV→arg→terminal priority)

Notes

  • TC-12 (tool classpath at runtime) is a system-level integration test, not unit-testable.
  • 4 pre-existing unrelated failures in env_spec, shell_init_spec, read_cookbook_for_compat_mode_upload_spec are not caused by this change (they touch no modified files).

Copilot AI lite review requested due to automatic review settings August 17, 2026 11:13
@sanghinitin
sanghinitin requested review from a team as code owners August 17, 2026 11:13
@sanghinitin sanghinitin added the Type: Enhancement Adds new functionality. label Aug 17, 2026
@github-actions

Copy link
Copy Markdown

Simplecov Report

Covered Threshold
98.51% 90%

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates chef gem forwarding so that, before remote-fetching gem subcommands run, Chef Premium’s RubyGems server (rubygems.chef.io) is automatically configured as a gem source when appropriate—enabling premium extension installs without manual gem sources --add setup.

Changes:

  • Adds source preflight logic to chef gem to detect existing sources, handle air-gapped/mirror scenarios, and add the Chef Premium source using a retrieved license key.
  • Adds unit tests covering the new source-detection and source-addition behavior, plus validation that the preflight runs before forwarding to RubyGems.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
lib/chef-cli/command/gem.rb Adds Chef Premium gem-source auto-configuration, including source detection, air-gap heuristics, and license-key retrieval integration.
spec/unit/command/gem_spec.rb Adds examples for ensure_chef_gem_source and add_chef_gem_source, plus verifies the preflight runs before gem command forwarding.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/chef-cli/command/gem.rb Outdated
Comment on lines +65 to +67
hosts = configured_source_hosts
return if hosts.include?(CHEF_GEM_SOURCE_HOST)

Comment thread lib/chef-cli/command/gem.rb Outdated
Comment on lines +92 to +98
def configured_source_hosts
Gem.sources.map do |source|
URI.parse(source.to_s).host
rescue URI::InvalidURIError
nil
end.compact
end
Copilot AI review requested due to automatic review settings August 17, 2026 11:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

lib/chef-cli/command/gem.rb:94

  • chef_gem_source_configured? treats an HTTP rubygems.chef.io source with v1 credentials as "configured". That would skip adding the HTTPS source and could leave users fetching premium gems over an insecure transport.
          uri = URI.parse(source.to_s)
          uri.host == CHEF_GEM_SOURCE_HOST && uri.user == "v1" && !uri.password.to_s.empty?
        rescue URI::InvalidURIError

Comment on lines +66 to +71
custom = non_standard_sources
unless custom.empty?
err("WARN: A custom gem source (#{custom.join(", ")}) is already configured; assuming an air-gapped environment.")
err("WARN: The Chef Premium RubyGem source was not added. Premium extensions may be unavailable.")
return
end
Copilot AI review requested due to automatic review settings August 17, 2026 13:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (3)

lib/chef-cli/command/gem.rb:69

  • The warning interpolates the full custom source URL(s) into output; if any custom source includes credentials (e.g., https://user:token@mirror/...), this will leak secrets into terminal logs/CI output. Prefer a message that doesn’t echo full source URLs (or redact userinfo before printing).
          err("WARN: A custom gem source (#{custom.join(", ")}) is already configured; assuming an air-gapped environment.")
          err("WARN: The Chef Premium RubyGem source was not added. Premium extensions may be unavailable.")

lib/chef-cli/command/gem.rb:94

  • chef_gem_source_configured? treats the Chef source as configured regardless of scheme. If a user has an insecure http://v1:<key>@rubygems.chef.io entry, this will be considered configured and the code will not add the secure HTTPS source.
          uri = URI.parse(source.to_s)
          uri.host == CHEF_GEM_SOURCE_HOST && uri.user == "v1" && !uri.password.to_s.empty?
        rescue URI::InvalidURIError

spec/unit/command/gem_spec.rb:274

  • The comment about file:// sources having host=="" is inaccurate for URIs like file:///var/cache/gems (URI.host is nil). This can confuse future maintenance/debugging of the air-gap detection behavior.
    # Bug fix: file:// sources have host=="" and must not be silently dropped from airgap detection

Copilot AI review requested due to automatic review settings August 19, 2026 05:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

lib/chef-cli/command/gem.rb:70

  • The warning logs the full configured custom gem source URLs (custom.join(", ")). If a custom source includes embedded credentials (e.g. https://user:pass@mirror.example), this will leak secrets to stderr/CI logs. Consider logging only host/scheme (or otherwise redacting userinfo) instead of echoing the full URL.
        custom = non_standard_sources
        unless custom.empty?
          err("WARN: A custom gem source (#{custom.join(", ")}) is already configured; assuming an air-gapped environment.")
          err("WARN: The Chef Premium RubyGem source was not added. Premium extensions may be unavailable.")
          return

lib/chef-cli/command/gem.rb:127

  • This PR description/acceptance criteria say the license key should be obtained via ChefLicensing with ENV → CLI arg → terminal prompt (and even references fetch_and_persist), but chef_license_key currently uses ChefLicensing.license_keys and explicitly avoids prompting. This is also inconsistent with ChefCLI::Licensing::Base.validate, which uses ChefLicensing.fetch_and_persist. Please confirm the intended UX and align the implementation + specs (including whether prompting is acceptable for chef gem install/search/fetch/...).
      # Fetches the first Chef license key from env, CLI args, or persisted storage.
      # Does not prompt the terminal — if no key is found nil is returned and the
      # caller warns the user to run `chef license add`.
      def chef_license_key
        keys = ChefLicensing.license_keys
        keys.is_a?(Array) ? keys.first : nil
      rescue StandardError

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

lib/chef-cli/command/gem.rb:86

  • The first non-flag token is not necessarily the gem subcommand: RubyGems accepts global options with separate values before the command, for example --config-file /path/gemsrc install knife. Here /path/gemsrc is selected as command, so source setup is skipped even though this is an install operation. Parse the RubyGems command/options (or account for option arguments) rather than assuming every non-flag token is a subcommand.
      def premium_source_command?(params)
        command = params.find { |p| !p.to_s.start_with?("-") }
        PREMIUM_SOURCE_COMMANDS.include?(command)

lib/chef-cli/command/gem.rb:68

  • Gem.sources entries can contain basic-auth userinfo, so interpolating the full custom URL into a warning can expose a mirror password in terminal output or captured logs. Report only the host (or redact userinfo) instead of joining the raw source URLs.
          err("WARN: A custom gem source (#{custom.join(", ")}) is already configured; assuming an air-gapped environment.")

lib/chef-cli/command/gem.rb:36

  • This classifies every install/search/update invocation as remote, including RubyGems' local-only forms such as chef gem install --local ./foo.gem. A local operation should not inspect licensing or persist a new source, but this path can add the Premium source (or emit a warning) before the local command runs. Exclude the local mode after parsing the subcommand/options.
      # gem subcommands that fetch from remote sources and need the Chef source configured.
      PREMIUM_SOURCE_COMMANDS = %w{install i search s fetch update download}.freeze

Comment on lines +124 to +126
def chef_license_key
keys = ChefLicensing.license_keys
keys.is_a?(Array) ? keys.first : nil
def chef_gem_source_configured?
Gem.sources.any? do |source|
uri = URI.parse(source.to_s)
uri.host == CHEF_GEM_SOURCE_HOST && uri.user == "v1" && !uri.password.to_s.empty?
Comment thread lib/chef-cli/command/gem.rb Outdated
Comment on lines +115 to +116
source_url = "https://v1:#{license_key}@#{CHEF_GEM_SOURCE_HOST}"
Gem::GemRunner.new.run(["sources", "--add", source_url])
Copilot AI review requested due to automatic review settings August 25, 2026 06:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (5)

Previously missed (1) — in code that hasn't changed since the last review.

lib/chef-cli/command/gem.rb:86

  • params.find assumes every option is boolean. RubyGems accepts valued global options before the subcommand (for example, gem --config-file /tmp/gemrc install knife or gem --source URL install knife), so this treats the option value as the command and skips source setup. The subsequent install can therefore run without the premium source; command detection needs to account for option arguments (or use RubyGems' command parsing).
      def premium_source_command?(params)
        command = params.find { |p| !p.to_s.start_with?("-") }
        PREMIUM_SOURCE_COMMANDS.include?(command)

lib/chef-cli/command/gem.rb:126

  • This path never receives params, so it cannot honor the documented --chef-license-key input, and calling license_keys does not invoke the fetch_and_persist ENV/argument/prompt flow described by the PR. A key supplied to chef gem is then still forwarded to GemRunner as an unknown RubyGems option, while an interactive fallback is never attempted. Parse and remove the Chef-specific option and use the licensing fetch path before forwarding the remaining arguments.
      def chef_license_key
        keys = ChefLicensing.license_keys
        keys.is_a?(Array) ? keys.first : nil

lib/chef-cli/command/gem.rb:68

  • custom contains the complete configured source URLs, including any embedded userinfo, and this warning prints them verbatim. A private mirror such as https://user:token@mirror.example/gems would therefore leak its credential to stderr and CI logs. Redact credentials or report only the source host/path before emitting this warning.
          err("WARN: A custom gem source (#{custom.join(", ")}) is already configured; assuming an air-gapped environment.")

lib/chef-cli/command/gem.rb:116

  • The nested sources --add invocation uses the full license-bearing URL, and RubyGems' sources command reports the URI when it succeeds (for example, https://v1:<key>@rubygems.chef.io added to sources). That exposes the license key in normal command output and CI logs on the first premium operation. Invoke the source command quietly or otherwise redact/suppress this output while retaining the persisted source.
        Gem::GemRunner.new.run(["sources", "--add", source_url])

lib/chef-cli/command/gem.rb:94

  • This predicate accepts an authenticated http://rubygems.chef.io entry as configured, so the command skips adding the secure HTTPS source and can perform premium gem requests over plaintext HTTP. Require the expected HTTPS scheme when deciding that the premium source is configured.
          uri.host == CHEF_GEM_SOURCE_HOST && uri.user == "v1" && !uri.password.to_s.empty?

Copilot AI review requested due to automatic review settings August 25, 2026 07:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

Previously missed (1) — in code that hasn't changed since the last review.

lib/chef-cli/command/gem.rb:63

  • The remote-source guard only checks the subcommand, so gem install --local ... (and local search/update) still reads licensing state and may persist the Chef source even though the operation cannot contact a remote source. Skip source setup when --local is present.

This issue also appears on line 84 of the same file.

        return unless premium_source_command?(params)

lib/chef-cli/command/gem.rb:68

  • This warning interpolates the raw entries from Gem.sources. A private mirror commonly uses a URL such as https://user:password@mirror/..., so this change can write credentials to stderr and CI logs. Log only a sanitized host or a generic air-gap warning instead of the complete source URI.
          err("WARN: A custom gem source (#{custom.join(", ")}) is already configured; assuming an air-gapped environment.")

lib/chef-cli/command/gem.rb:125

  • This uses ChefLicensing.license_keys instead of the fetch_and_persist path used by the existing license command. Consequently, an unpersisted key from the environment/CLI and the terminal fallback are not considered, so the new flow warns that no key exists and skips the premium source despite the documented ENV → argument → prompt priority. Use the fetching API and wire the command argument according to its contract.
        keys = ChefLicensing.license_keys

lib/chef-cli/command/gem.rb:94

  • chef_gem_source_configured? accepts any URI scheme, so a configured source such as http://v1:<key>@rubygems.chef.io is treated as valid and prevents the secure HTTPS source from being added. Premium requests and the credential can then use plaintext HTTP. Require HTTPS and ensure an insecure Chef entry is not retained/used when classifying configured sources.
          uri.host == CHEF_GEM_SOURCE_HOST && uri.user == "v1" && !uri.password.to_s.empty?

lib/chef-cli/command/gem.rb:86

  • The command detector only skips tokens beginning with -; it does not account for GemRunner options that consume a separate value. For example, chef gem --config-file /tmp/gemrc install knife identifies /tmp/gemrc as the command and skips source setup. Parse the GemRunner options or otherwise skip their argument tokens before locating the subcommand.
      def premium_source_command?(params)
        command = params.find { |p| !p.to_s.start_with?("-") }
        PREMIUM_SOURCE_COMMANDS.include?(command)

lib/chef-cli/command/gem.rb:116

  • When the existing source is the bare Chef URL, this adds an authenticated URL but leaves the unauthenticated entry in Gem.sources. RubyGems keeps these as distinct sources, so the old entry remains eligible for requests and an unauthorized response can still make the install fail. Remove or replace existing Chef-host entries before adding the authenticated source.
      def add_chef_gem_source(license_key)
        source_url = "https://" + "v1:#{license_key}" + "@#{CHEF_GEM_SOURCE_HOST}"
        Gem::GemRunner.new.run(["sources", "--add", source_url])

lib/chef-cli/command/gem.rb:36

  • RubyGems supports up as an alias for update, but this list omits it while including the i and s aliases. chef gem up therefore bypasses the source check and can fail to access premium extensions. Add the update alias.
      PREMIUM_SOURCE_COMMANDS = %w{install i search s fetch update download}.freeze

Comment on lines +115 to +116
source_url = "https://" + "v1:#{license_key}" + "@#{CHEF_GEM_SOURCE_HOST}"
Gem::GemRunner.new.run(["sources", "--add", source_url])
Signed-off-by: nitin sanghi <nsanghi@progress.com>
@sanghinitin
sanghinitin force-pushed the CHEF-34005-chef-ruby-server branch from 344b63b to 3854fef Compare August 25, 2026 08:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted Type: Enhancement Adds new functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants