Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions lib/chef/knife.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,33 @@ def self.reset_subcommands!
def self.inherited(subclass)
super
unless subclass.unnamed?
caller_path = if subclass.superclass.to_s == "Chef::ChefFS::Knife"
# ChefFS-based commands have a superclass that defines an
# inherited method which calls super. This means that the
# top of the call stack is not the class definition for
# our subcommand. Try the second entry in the call stack.
path_from_caller(caller[1])
else
path_from_caller(caller[0])
end

# Skip classes defined inside a subdirectory of the lib/chef/knife/ tree
# (e.g. lib/chef/knife/cloud/server/create_command.rb). By convention,
# real knife subcommands live directly at lib/chef/knife/<name>.rb —
# the same flat pattern that GemGlobLoader uses when discovering
# commands. Abstract base classes from plugins like knife-cloud are
# nested in subdirs and are only loaded transitively; registering them
# causes spurious categories (e.g. ** SERVER COMMANDS **) to appear in
# 'knife --help'.
#
# NOTE: The regex anchors to /lib/chef/knife/ to avoid false positives
# from Habitat package paths, where the package origin and name appear
# in the path (e.g. /hab/pkgs/chef/knife/19.0.99/…) and would otherwise
# match a looser pattern like /chef/knife/[^/]+/.
return if caller_path.match?(%r{/lib/chef/knife/[^/]+/})

subcommands[subclass.snake_case_name] = subclass
subcommand_files[subclass.snake_case_name] +=
if subclass.superclass.to_s == "Chef::ChefFS::Knife"
# ChefFS-based commands have a superclass that defines an
# inherited method which calls super. This means that the
# top of the call stack is not the class definition for
# our subcommand. Try the second entry in the call stack.
[path_from_caller(caller[1])]
else
[path_from_caller(caller[0])]
end
subcommand_files[subclass.snake_case_name] += [caller_path]
end
end

Expand Down
62 changes: 62 additions & 0 deletions spec/unit/knife_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,68 @@ class AwesomeCheffsCommand < Chef::ChefFS::Knife

end

describe "subdirectory subcommand filtering" do
before do
Chef::Knife.reset_subcommands!
end

after do
%i{CloudSubdirCommand FlatKnifeCommand HabitatKnifeCommand CheffsSubdirCommand}.each do |const|
KnifeSpecs.send(:remove_const, const) if KnifeSpecs.const_defined?(const)
end
end

it "does not register subcommands defined in a subdirectory of lib/chef/knife/" do
allow(Chef::Knife).to receive(:path_from_caller)
.and_return("/some/gem/lib/chef/knife/cloud/server/create_command.rb")

module KnifeSpecs
class CloudSubdirCommand < Chef::Knife; end
end

expect(Chef::Knife.subcommands).not_to have_key("cloud_subdir_command")
expect(Chef::Knife.subcommand_files).not_to have_key("cloud_subdir_command")
end

it "registers subcommands defined directly in lib/chef/knife/ (flat path)" do
allow(Chef::Knife).to receive(:path_from_caller)
.and_return("/some/gem/lib/chef/knife/flat_knife_command.rb")

module KnifeSpecs
class FlatKnifeCommand < Chef::Knife; end
end

expect(Chef::Knife.subcommands).to have_key("flat_knife_command")
expect(Chef::Knife.subcommand_files["flat_knife_command"]).to eq(["/some/gem/lib/chef/knife/flat_knife_command.rb"])
end

it "does not filter subcommands loaded from Habitat package paths" do
# Habitat paths contain /chef/knife/ as the package origin/name segment,
# e.g. /hab/pkgs/chef/knife/19.0.99/lib/chef/knife/foo.rb. The regex must
# anchor to /lib/chef/knife/ to avoid false positives on these paths.
allow(Chef::Knife).to receive(:path_from_caller)
.and_return("/hab/pkgs/chef/knife/19.0.99/lib/chef/knife/habitat_knife_command.rb")

module KnifeSpecs
class HabitatKnifeCommand < Chef::Knife; end
end

expect(Chef::Knife.subcommands).to have_key("habitat_knife_command")
end

it "does not register ChefFS-based subcommands defined in subdirectories of lib/chef/knife/" do
# ChefFS commands use caller[1] for path detection; filtering should still apply.
allow(Chef::Knife).to receive(:path_from_caller)
.and_return("/some/gem/lib/chef/knife/cloud/cheffs_subdir_command.rb")

module KnifeSpecs
class CheffsSubdirCommand < Chef::ChefFS::Knife; end
end

expect(Chef::Knife.subcommands).not_to have_key("cheffs_subdir_command")
end
end

describe "the headers include X-Remote-Request-Id" do

let(:headers) do
Expand Down
Loading