CHEF-34005: Auto-configure Chef Premium RubyGem server as gem source - #349
CHEF-34005: Auto-configure Chef Premium RubyGem server as gem source#349sanghinitin wants to merge 1 commit into
Conversation
Simplecov Report
|
There was a problem hiding this comment.
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 gemto 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.
| hosts = configured_source_hosts | ||
| return if hosts.include?(CHEF_GEM_SOURCE_HOST) | ||
|
|
| def configured_source_hosts | ||
| Gem.sources.map do |source| | ||
| URI.parse(source.to_s).host | ||
| rescue URI::InvalidURIError | ||
| nil | ||
| end.compact | ||
| end |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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.ioentry, 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 likefile:///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
There was a problem hiding this comment.
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), butchef_license_keycurrently usesChefLicensing.license_keysand explicitly avoids prompting. This is also inconsistent withChefCLI::Licensing::Base.validate, which usesChefLicensing.fetch_and_persist. Please confirm the intended UX and align the implementation + specs (including whether prompting is acceptable forchef 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
There was a problem hiding this comment.
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/gemsrcis selected ascommand, so source setup is skipped even though this is aninstalloperation. 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.sourcesentries 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/updateinvocation as remote, including RubyGems' local-only forms such aschef 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
| 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? |
| source_url = "https://v1:#{license_key}@#{CHEF_GEM_SOURCE_HOST}" | ||
| Gem::GemRunner.new.run(["sources", "--add", source_url]) |
There was a problem hiding this comment.
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.findassumes every option is boolean. RubyGems accepts valued global options before the subcommand (for example,gem --config-file /tmp/gemrc install knifeorgem --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-keyinput, and callinglicense_keysdoes not invoke thefetch_and_persistENV/argument/prompt flow described by the PR. A key supplied tochef gemis then still forwarded toGemRunneras 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
customcontains the complete configured source URLs, including any embedded userinfo, and this warning prints them verbatim. A private mirror such ashttps://user:token@mirror.example/gemswould 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 --addinvocation 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.ioentry 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?
There was a problem hiding this comment.
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 localsearch/update) still reads licensing state and may persist the Chef source even though the operation cannot contact a remote source. Skip source setup when--localis 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 ashttps://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_keysinstead of thefetch_and_persistpath 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 ashttp://v1:<key>@rubygems.chef.iois 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 knifeidentifies/tmp/gemrcas 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
upas an alias forupdate, but this list omits it while including theiandsaliases.chef gem uptherefore 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
| 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>
344b63b to
3854fef
Compare
Summary
Automatically configure the Chef Premium RubyGem server (
rubygems.chef.io) as a gem source before anychef gem install,search,fetch,update, ordownloadoperation, 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 rungem source --add https://v1:\<key\>@rubygems.chef.iobeforechef gem installwould work.Solution
The
chef gemcommand now checks the configured gem sources before any remote-fetching subcommand and takes the appropriate action:Files Changed
lib/chef-cli/command/gem.rbensure_chef_gem_source,premium_source_command?,configured_source_hosts,add_chef_gem_source,chef_license_keyspec/unit/command/gem_spec.rbTest Evidence
Acceptance Criteria Coverage
fetch_and_persistcalled (preserves ENV→arg→terminal priority)Notes
env_spec,shell_init_spec,read_cookbook_for_compat_mode_upload_specare not caused by this change (they touch no modified files).