knife 18.8.68 → 19.0.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +30 -9
  3. data/LICENSE +201 -201
  4. data/Rakefile +23 -0
  5. data/knife.gemspec +9 -15
  6. data/lib/chef/application/knife.rb +3 -5
  7. data/lib/chef/chef_fs/knife.rb +1 -1
  8. data/lib/chef/knife/acl_base.rb +1 -1
  9. data/lib/chef/knife/acl_bulk_add.rb +0 -1
  10. data/lib/chef/knife/acl_bulk_remove.rb +0 -1
  11. data/lib/chef/knife/bootstrap/templates/chef-full.erb +24 -5
  12. data/lib/chef/knife/bootstrap/templates/windows-chef-client-msi.erb +121 -4
  13. data/lib/chef/knife/bootstrap.rb +25 -29
  14. data/lib/chef/knife/config_get.rb +1 -1
  15. data/lib/chef/knife/config_get_profile.rb +1 -1
  16. data/lib/chef/knife/config_list_profiles.rb +1 -1
  17. data/lib/chef/knife/config_show.rb +1 -1
  18. data/lib/chef/knife/config_use_profile.rb +3 -3
  19. data/lib/chef/knife/configure_client.rb +1 -1
  20. data/lib/chef/knife/cookbook_bulk_delete.rb +1 -1
  21. data/lib/chef/knife/cookbook_delete.rb +1 -1
  22. data/lib/chef/knife/cookbook_download.rb +1 -1
  23. data/lib/chef/knife/cookbook_upload.rb +1 -1
  24. data/lib/chef/knife/core/bootstrap_context.rb +36 -6
  25. data/lib/chef/knife/core/cookbook_scm_repo.rb +2 -2
  26. data/lib/chef/knife/core/cookbook_site_streaming_uploader.rb +1 -6
  27. data/lib/chef/knife/core/gem_glob_loader.rb +4 -4
  28. data/lib/chef/knife/core/object_loader.rb +1 -1
  29. data/lib/chef/knife/core/status_presenter.rb +1 -1
  30. data/lib/chef/knife/core/subcommand_loader.rb +1 -1
  31. data/lib/chef/knife/core/text_formatter.rb +0 -1
  32. data/lib/chef/knife/core/windows_bootstrap_context.rb +103 -37
  33. data/lib/chef/knife/data_bag_secret_options.rb +1 -1
  34. data/lib/chef/knife/download.rb +1 -1
  35. data/lib/chef/knife/environment_compare.rb +2 -3
  36. data/lib/chef/knife/key_create.rb +1 -5
  37. data/lib/chef/knife/key_edit.rb +1 -5
  38. data/lib/chef/knife/license.rb +6 -2
  39. data/lib/chef/knife/list.rb +1 -1
  40. data/lib/chef/knife/node_edit.rb +1 -1
  41. data/lib/chef/knife/raw.rb +1 -2
  42. data/lib/chef/knife/recipe_list.rb +1 -1
  43. data/lib/chef/knife/search.rb +2 -2
  44. data/lib/chef/knife/ssh.rb +5 -5
  45. data/lib/chef/knife/ssl_fetch.rb +1 -1
  46. data/lib/chef/knife/supermarket_share.rb +3 -2
  47. data/lib/chef/knife/upload.rb +1 -1
  48. data/lib/chef/knife/user_create.rb +11 -21
  49. data/lib/chef/knife/user_list.rb +3 -3
  50. data/lib/chef/knife/version.rb +1 -3
  51. data/lib/chef/knife/xargs.rb +0 -1
  52. data/lib/chef/knife.rb +4 -1
  53. data/lib/chef/utils/licensing_config.rb +0 -1
  54. data/lib/chef/utils/licensing_handler.rb +18 -28
  55. data/spec/data/apt/chef-integration-test-1.0/debian/rules +0 -0
  56. data/spec/data/apt/chef-integration-test-1.1/debian/rules +0 -0
  57. data/spec/data/apt/chef-integration-test2-1.0/debian/rules +0 -0
  58. data/spec/data/cookbooks/openldap/templates/default/all_windows_line_endings.erb +4 -4
  59. data/spec/data/cookbooks/openldap/templates/default/some_windows_line_endings.erb +2 -2
  60. data/spec/functional/cookbook_delete_spec.rb +4 -4
  61. data/spec/functional/version_spec.rb +2 -3
  62. data/spec/integration/config_use_spec.rb +8 -8
  63. data/spec/integration/cookbook_api_ipv6_spec.rb +2 -2
  64. data/spec/integration/node_environment_set_spec.rb +1 -1
  65. data/spec/integration/node_run_list_set_spec.rb +1 -1
  66. data/spec/knife_spec_helper.rb +0 -5
  67. data/spec/support/platforms/prof/gc.rb +1 -1
  68. data/spec/support/shared/integration/integration_helper.rb +1 -1
  69. data/spec/tiny_server.rb +2 -2
  70. data/spec/unit/knife/bootstrap/templates/windows_presigned_url_spec.rb +183 -0
  71. data/spec/unit/knife/bootstrap_chef19_spec.rb +122 -0
  72. data/spec/unit/knife/bootstrap_spec.rb +121 -7
  73. data/spec/unit/knife/configure_spec.rb +3 -3
  74. data/spec/unit/knife/cookbook_list_spec.rb +1 -1
  75. data/spec/unit/knife/core/bootstrap_context_spec.rb +137 -0
  76. data/spec/unit/knife/core/windows_bootstrap_context_chef19_spec.rb +82 -0
  77. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +51 -64
  78. data/spec/unit/knife/license_spec.rb +1 -1
  79. data/spec/unit/knife/search_spec.rb +3 -3
  80. data/spec/unit/knife/supermarket_install_spec.rb +4 -5
  81. data/spec/unit/knife/utils/licensing_handler_chef19_spec.rb +144 -0
  82. metadata +61 -100
  83. data/spec/data/gems/chef-integration-test-0.1.0.gem +0 -0
  84. data/spec/data/nodes/default.rb +0 -15
  85. data/spec/data/nodes/test.example.com.rb +0 -17
  86. data/spec/data/nodes/test.rb +0 -15
  87. data/spec/unit/utils/licensing_handler_spec.rb +0 -140
@@ -75,7 +75,7 @@ describe Chef::Knife::CookbookDelete do
75
75
 
76
76
  knife.run
77
77
 
78
- expect(stdout.string).to match(/#{Regexp.escape('Do you really want to delete obsolete-cookbook version 1.0.0? (Y/N)')}/)
78
+ expect(stdout.string).to match(/#{Regexp.escape("Do you really want to delete obsolete-cookbook version 1.0.0? (Y/N)")}/)
79
79
  expect(cb100_deleted).to be_truthy
80
80
  end
81
81
 
@@ -91,8 +91,8 @@ describe Chef::Knife::CookbookDelete do
91
91
 
92
92
  knife.run
93
93
 
94
- expect(stdout.string).to match(/#{Regexp.escape('Are you sure you want to purge files')}/)
95
- expect(stdout.string).to match(/#{Regexp.escape('Do you really want to delete obsolete-cookbook version 1.0.0? (Y/N)')}/)
94
+ expect(stdout.string).to match(/#{Regexp.escape("Are you sure you want to purge files")}/)
95
+ expect(stdout.string).to match(/#{Regexp.escape("Do you really want to delete obsolete-cookbook version 1.0.0? (Y/N)")}/)
96
96
  expect(cb100_deleted).to be_truthy
97
97
 
98
98
  end
@@ -123,7 +123,7 @@ describe Chef::Knife::CookbookDelete do
123
123
  end
124
124
 
125
125
  it "asks which version to delete and deletes that when not given the -a flag" do
126
- cb100_deleted = cb110_deleted = cb120_deleted = nil
126
+ cb100_deleted = nil
127
127
  api.delete("/cookbooks/obsolete-cookbook/1.0.0", 200) { cb100_deleted = true; "[\"true\"]" }
128
128
  stdin, stdout = StringIO.new, StringIO.new
129
129
  allow(knife.ui).to receive(:stdin).and_return(stdin)
@@ -18,9 +18,8 @@ require "chef/mixin/shell_out"
18
18
 
19
19
  describe "Knife Version", :executables do
20
20
  include Chef::Mixin::ShellOut
21
- let(:knife_dir) { File.join(__dir__, "..", "..", "..", "knife") }
22
- xit "should be sane" do
21
+ let(:knife_dir) { File.join(__dir__, "..", "..") }
22
+ it "should be same" do
23
23
  expect(shell_out!("bundle exec knife -v", cwd: knife_dir).stdout.chomp).to match(/.*: #{Chef::Knife::VERSION}/)
24
24
  end
25
25
  end
26
-
@@ -159,10 +159,10 @@ describe "knife config use", :workstation do
159
159
  before do
160
160
  ENV["CHEF_HOME"] = path_to("chefhome"); file("chefhome/tmp", "")
161
161
  file("chefhome/.chef/credentials", <<~EOH
162
- [staging]
163
- client_name = "testuser"
164
- client_key = "testkey.pem"
165
- chef_server_url = "https://example.com/organizations/testorg"
162
+ [staging]
163
+ client_name = "testuser"
164
+ client_key = "testkey.pem"
165
+ chef_server_url = "https://example.com/organizations/testorg"
166
166
  EOH
167
167
  )
168
168
  end
@@ -180,10 +180,10 @@ describe "knife config use", :workstation do
180
180
  before do
181
181
  ENV["KNIFE_HOME"] = path_to("knifehome"); file("knifehome/tmp", "")
182
182
  file("knifehome/.chef/credentials", <<~EOH
183
- [development]
184
- client_name = "testuser"
185
- client_key = "testkey.pem"
186
- chef_server_url = "https://example.com/organizations/testorg"
183
+ [development]
184
+ client_name = "testuser"
185
+ client_key = "testkey.pem"
186
+ chef_server_url = "https://example.com/organizations/testorg"
187
187
  EOH
188
188
  )
189
189
  end
@@ -19,7 +19,7 @@ require "knife_spec_helper"
19
19
  require "support/shared/integration/integration_helper"
20
20
  require "chef/mixin/shell_out"
21
21
 
22
- describe "Knife cookbook API integration with IPv6", :workstation, :not_supported_on_gce do
22
+ describe "Knife cookbook API integration with IPv6", :workstation do
23
23
  include IntegrationSupport
24
24
  include Chef::Mixin::ShellOut
25
25
 
@@ -62,7 +62,7 @@ describe "Knife cookbook API integration with IPv6", :workstation, :not_supporte
62
62
  Dir.mktmpdir
63
63
  end
64
64
 
65
- let(:chef_dir) { File.join(__dir__, "..", "..", "..", "knife", "bin") }
65
+ let(:chef_dir) { File.join(__dir__, "..", "..", "bin") }
66
66
  let(:knife) { "ruby '#{chef_dir}/knife'" }
67
67
 
68
68
  let(:knife_config_flag) { "-c '#{path_to("config/knife.rb")}'" }
@@ -40,7 +40,7 @@ describe "knife node environment set", :workstation do
40
40
 
41
41
  it "with no environment" do
42
42
  knife("node environment set adam").should_fail stderr: "FATAL: You must specify a node name and an environment.\n",
43
- stdout: /^USAGE: knife node environment set NODE ENVIRONMENT\n/
43
+ stdout: /^USAGE: knife node environment set NODE ENVIRONMENT\n/
44
44
  end
45
45
  end
46
46
  end
@@ -35,7 +35,7 @@ describe "knife node run list set", :workstation do
35
35
 
36
36
  it "with no role or recipe" do
37
37
  knife("node run list set cons").should_fail stderr: "FATAL: You must supply both a node name and a run list.\n",
38
- stdout: /^USAGE: knife node run_list set NODE ENTRIES \(options\)/m
38
+ stdout: /^USAGE: knife node run_list set NODE ENTRIES \(options\)/m
39
39
  end
40
40
  end
41
41
  end
@@ -98,11 +98,6 @@ TEST_PLATFORM = TEST_NODE["platform"]
98
98
  TEST_PLATFORM_VERSION = TEST_NODE["platform_version"]
99
99
  TEST_PLATFORM_FAMILY = TEST_NODE["platform_family"]
100
100
 
101
- provider_priority_map ||= nil
102
- resource_priority_map ||= nil
103
- provider_handler_map ||= nil
104
- resource_handler_map ||= nil
105
-
106
101
  class UnexpectedSystemExit < RuntimeError
107
102
  def self.from(system_exit)
108
103
  new(system_exit.message).tap { |e| e.set_backtrace(system_exit.backtrace) }
@@ -24,7 +24,7 @@ module RSpec
24
24
  # GC 1 invokes.
25
25
  # Index Invoke Time(sec) Use Size(byte) Total Size(byte) Total Object GC time(ms)
26
26
  # 1 0.012 159240 212940 10647 0.00000000000001530000
27
- LINE_PATTERN = /^\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)$/.freeze
27
+ LINE_PATTERN = /^\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)$/
28
28
 
29
29
  def start
30
30
  ::GC::Profiler.enable unless ::GC::Profiler.enabled?
@@ -59,7 +59,7 @@ module IntegrationSupport
59
59
  # TODO: "force" actually means "silence all exceptions". this
60
60
  # silences a weird permissions error on Windows that we should track
61
61
  # down, but for now there's no reason for it to blow up our CI.
62
- FileUtils.remove_entry_secure(@repository_dir, force = ChefUtils.windows?)
62
+ FileUtils.remove_entry_secure(@repository_dir, ChefUtils.windows?)
63
63
  ensure
64
64
  @repository_dir = nil
65
65
  end
data/spec/tiny_server.rb CHANGED
@@ -132,7 +132,7 @@ module TinyServer
132
132
  end
133
133
 
134
134
  def call(env)
135
- if response = response_for_request(env)
135
+ if (response = response_for_request(env))
136
136
  response.call
137
137
  else
138
138
  debug_info = { message: "no data matches the request for #{env["REQUEST_URI"]}",
@@ -144,7 +144,7 @@ module TinyServer
144
144
  end
145
145
 
146
146
  def response_for_request(env)
147
- if route = @routes[env["REQUEST_METHOD"]].find { |route| route.matches_request?(env["REQUEST_URI"]) }
147
+ if (route = @routes[env["REQUEST_METHOD"]].find { |route| route.matches_request?(env["REQUEST_URI"]) })
148
148
  route.response
149
149
  end
150
150
  end
@@ -0,0 +1,183 @@
1
+ #
2
+ # Copyright:: Copyright (c) Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "knife_spec_helper"
19
+ require "chef/knife/bootstrap"
20
+ require "chef/knife/core/windows_bootstrap_context"
21
+
22
+ describe "windows-chef-client-msi.erb template with presigned URLs" do
23
+ let(:connection) { double("Train::Connection") }
24
+ let(:knife) do
25
+ k = Chef::Knife::Bootstrap.new(["-o", "winrm", "127.0.0.1"])
26
+ allow(k).to receive(:connection).and_return(connection)
27
+ allow(connection).to receive(:windows?).and_return(true)
28
+ allow(connection).to receive(:hostname).and_return("test.example.com")
29
+ allow(connection).to receive(:os).and_return(OpenStruct.new(family: "windows"))
30
+ k
31
+ end
32
+
33
+ describe "with presigned S3 URL containing special characters" do
34
+ let(:presigned_url) do
35
+ "https://workstation-rc3-assets.s3.us-east-1.amazonaws.com/install.ps1" \
36
+ "?response-content-disposition=inline" \
37
+ "&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD" \
38
+ "&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHQaCXVzLWVhc3QtMSJIMEYCIQCfUHkC2e0%2FzlRkBc%2F2dgmp" \
39
+ "&X-Amz-Algorithm=AWS4-HMAC-SHA256" \
40
+ "&X-Amz-Credential=ASIA5LOKXA7I3346RT45%2F20251112%2Fus-east-1%2Fs3%2Faws4_request" \
41
+ "&X-Amz-Date=20251112T194920Z" \
42
+ "&X-Amz-Expires=43200" \
43
+ "&X-Amz-SignedHeaders=host" \
44
+ "&X-Amz-Signature=34d6c57d80c9a59d9f524069aa12739cf50d45b907d2a1d27e41fbf76828ad3c"
45
+ end
46
+
47
+ before do
48
+ knife.config[:bootstrap_url] = presigned_url
49
+ end
50
+
51
+ let(:rendered_template) { knife.render_template }
52
+
53
+ it "properly escapes percent signs in the URL for batch variable assignment" do
54
+ # Verify that percent signs are doubled for batch file handling
55
+ expect(rendered_template).to include('set "BOOTSTRAP_DOWNLOAD_URL=')
56
+ # Check that percent signs are properly escaped in the environment variable
57
+ expect(rendered_template).to match(/set "BOOTSTRAP_DOWNLOAD_URL=.*%%2F.*%%2F/)
58
+ end
59
+
60
+ it "uses environment variable to pass URL to PowerShell for download" do
61
+ # Verify that the URL is passed via environment variable to avoid batch expansion issues
62
+ expect(rendered_template).to include("$env:BOOTSTRAP_DOWNLOAD_URL")
63
+ expect(rendered_template).to include("powershell.exe -ExecutionPolicy Unrestricted")
64
+ # Verify download uses -Command parameter with script block for custom URLs
65
+ expect(rendered_template).to match(/powershell\.exe -ExecutionPolicy Unrestricted -InputFormat None -NoProfile -NonInteractive -Command "& '.*wget\.ps1'/)
66
+ end
67
+
68
+ it "uses -File parameter for install script execution" do
69
+ # Verify that the install script execution uses -File parameter for custom bootstrap URLs
70
+ expect(rendered_template).to match(/powershell\.exe -ExecutionPolicy Unrestricted -File "%LOCAL_DESTINATION_SCRIPT_PATH%"/)
71
+ end
72
+
73
+ it "does not use batch variable expansion for custom bootstrap URLs" do
74
+ # Verify that we're not using %REMOTE_SOURCE_SCRIPT_URL% expansion for custom URLs
75
+ # which would cause the percent signs to be misinterpreted
76
+ lines_with_bootstrap_url = rendered_template.lines.select { |line| line.include?("BOOTSTRAP_DOWNLOAD_URL") }
77
+ expect(lines_with_bootstrap_url).not_to be_empty
78
+
79
+ # The custom URL should not go through %VAR% expansion
80
+ expect(rendered_template).not_to include("%BOOTSTRAP_DOWNLOAD_URL%")
81
+ end
82
+
83
+ it "properly handles the URL in the cscript fallback" do
84
+ # Verify that the cscript fallback also uses the environment variable
85
+ expect(rendered_template).to include("cscript /nologo")
86
+ expect(rendered_template).to match(/wget\.vbs.*BOOTSTRAP_DOWNLOAD_URL/)
87
+ end
88
+
89
+ it "preserves all query parameters in the URL" do
90
+ # Verify that important query parameters are preserved
91
+ expect(rendered_template).to include("X-Amz-Algorithm=AWS4-HMAC-SHA256")
92
+ expect(rendered_template).to include("X-Amz-Credential=")
93
+ expect(rendered_template).to include("X-Amz-Date=")
94
+ expect(rendered_template).to include("X-Amz-Signature=")
95
+ end
96
+
97
+ it "does not corrupt encoded slashes in the credential parameter" do
98
+ # The most critical part - encoded slashes (%2F) must remain as %%2F in batch
99
+ # so they survive batch processing and become %2F when passed to PowerShell
100
+ expect(rendered_template).to match(/ASIA5LOKXA7I3346RT45%%2F20251112%%2Fus-east-1%%2Fs3%%2Faws4_request/)
101
+ end
102
+
103
+ it "does not corrupt encoded plus signs" do
104
+ # Verify that %2B (encoded +) becomes %%2B
105
+ if presigned_url.include?("%2B")
106
+ expect(rendered_template).to include("%%2B")
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "with standard bootstrap URL (no special characters)" do
112
+ before do
113
+ knife.config[:license_url] = "https://chefdownload-trial.chef.io/install.sh?license_id=test-license"
114
+ end
115
+
116
+ let(:rendered_template) { knife.render_template }
117
+
118
+ it "uses REMOTE_SOURCE_SCRIPT_URL for standard URLs" do
119
+ expect(rendered_template).to include('set "REMOTE_SOURCE_SCRIPT_URL=')
120
+ expect(rendered_template).to include("%REMOTE_SOURCE_SCRIPT_URL%")
121
+ end
122
+
123
+ it "uses license URL when no bootstrap_url is provided" do
124
+ expect(rendered_template).to include("https://chefdownload-trial.chef.io/install.ps1?license_id=test-license")
125
+ end
126
+ end
127
+
128
+ describe "edge cases" do
129
+ context "when bootstrap URL contains single quotes" do
130
+ before do
131
+ knife.config[:bootstrap_url] = "https://example.com/install.ps1?param='value'"
132
+ end
133
+
134
+ let(:rendered_template) { knife.render_template }
135
+
136
+ it "properly escapes single quotes in PowerShell command" do
137
+ # Single quotes should be doubled for PowerShell string escaping
138
+ expect(rendered_template).to include("''")
139
+ end
140
+ end
141
+
142
+ context "when bootstrap URL contains ampersands" do
143
+ before do
144
+ knife.config[:bootstrap_url] = "https://example.com/install.ps1?a=1&b=2&c=3"
145
+ end
146
+
147
+ let(:rendered_template) { knife.render_template }
148
+
149
+ it "preserves ampersands in the URL" do
150
+ expect(rendered_template).to include("&b=2&")
151
+ expect(rendered_template).to include("&c=3")
152
+ end
153
+ end
154
+
155
+ context "when bootstrap URL contains equals signs" do
156
+ before do
157
+ knife.config[:bootstrap_url] = "https://example.com/install.ps1?key=value"
158
+ end
159
+
160
+ let(:rendered_template) { knife.render_template }
161
+
162
+ it "preserves equals signs in the URL" do
163
+ expect(rendered_template).to include("key=value")
164
+ end
165
+ end
166
+ end
167
+
168
+ describe "error messages" do
169
+ before do
170
+ knife.config[:bootstrap_url] = "https://example.com/install.ps1?X-Amz-Signature=abc123"
171
+ end
172
+
173
+ let(:rendered_template) { knife.render_template }
174
+
175
+ it "displays appropriate error message for custom URLs" do
176
+ expect(rendered_template).to include("Failed to download install script")
177
+ # Should not reference %REMOTE_SOURCE_SCRIPT_URL% for custom URLs
178
+ error_section = rendered_template.split("if NOT %DOWNLOAD_ERROR_STATUS%==0").last
179
+ custom_url_section = error_section.split("else").first
180
+ expect(custom_url_section).not_to include("%REMOTE_SOURCE_SCRIPT_URL%")
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,122 @@
1
+ #
2
+ # Copyright:: Copyright (c) Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "knife_spec_helper"
19
+ require "chef/knife/bootstrap"
20
+
21
+ describe Chef::Knife::Bootstrap do
22
+ let(:knife) { described_class.new }
23
+ let(:stdout) { StringIO.new }
24
+ let(:stderr) { StringIO.new }
25
+ let(:stdin) { StringIO.new }
26
+
27
+ before do
28
+ knife.ui = Chef::Knife::UI.new(stdout, stderr, stdin, {})
29
+ knife.name_args = ["test-node"]
30
+ end
31
+
32
+ describe "Chef Infra 19 licensing support" do
33
+ describe "#fetch_license" do
34
+ let(:bootstrap_context) { double("bootstrap_context") }
35
+ let(:license_handler) { double("license_handler") }
36
+
37
+ before do
38
+ allow(Chef::Knife::Core::BootstrapContext).to receive(:new).and_return(bootstrap_context)
39
+ allow(Chef::Utils::LicensingHandler).to receive(:validate!).and_return(license_handler)
40
+ allow(license_handler).to receive(:install_sh_url).and_return("https://example.com/install.sh")
41
+ allow(license_handler).to receive(:license_key).and_return("test-key")
42
+ allow(license_handler).to receive(:omnitruck_url).and_return("https://example.com/%s")
43
+ allow(license_handler).to receive(:license_type).and_return("trial")
44
+ end
45
+
46
+ context "when license is required for Chef Infra 19" do
47
+ before do
48
+ allow(bootstrap_context).to receive(:chef_ice?).and_return(true)
49
+ end
50
+
51
+ it "fetches and sets license configuration" do
52
+ expect(ChefLicensing::Config).to receive(:require_license_for).and_yield
53
+
54
+ knife.config[:bootstrap_version] = "19.0.0"
55
+ knife.send(:fetch_license)
56
+
57
+ expect(knife.config[:license_url]).to eq("https://example.com/install.sh")
58
+ expect(knife.config[:license_id]).to eq("test-key")
59
+ expect(knife.config[:omnitruck_url]).to eq("https://example.com/%s")
60
+ expect(knife.config[:license_type]).to eq("trial")
61
+ end
62
+ end
63
+
64
+ context "when custom bootstrap URL is provided" do
65
+ it "skips license fetching" do
66
+ expect(ChefLicensing::Config).not_to receive(:require_license_for)
67
+
68
+ knife.config[:bootstrap_url] = "https://custom.example.com/install.sh"
69
+ knife.send(:fetch_license)
70
+
71
+ expect(knife.config[:license_url]).to be_nil
72
+ end
73
+ end
74
+
75
+ context "when custom bootstrap template is provided" do
76
+ it "skips license fetching" do
77
+ expect(ChefLicensing::Config).not_to receive(:require_license_for)
78
+
79
+ knife.config[:bootstrap_template] = "custom-template"
80
+ knife.send(:fetch_license)
81
+
82
+ expect(knife.config[:license_url]).to be_nil
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "command line options" do
88
+ it "accepts --bootstrap-product option" do
89
+ knife.config[:bootstrap_product] = "chef-ice"
90
+ expect(knife.config[:bootstrap_product]).to eq("chef-ice")
91
+ end
92
+
93
+ it "accepts --license-key option" do
94
+ knife.config[:chef_license_key] = "test-license-key"
95
+ expect(knife.config[:chef_license_key]).to eq("test-license-key")
96
+ end
97
+
98
+ it "accepts --license-url option" do
99
+ knife.config[:chef_license_server] = "https://example.com/license"
100
+ expect(knife.config[:chef_license_server]).to eq("https://example.com/license")
101
+ end
102
+ end
103
+
104
+ describe "option descriptions" do
105
+ let(:bootstrap_product_option) { knife.class.options[:bootstrap_product] }
106
+ let(:license_key_option) { knife.class.options[:chef_license_key] }
107
+ let(:license_url_option) { knife.class.options[:chef_license_server] }
108
+
109
+ it "has proper description for bootstrap_product" do
110
+ expect(bootstrap_product_option[:description]).to include("Product")
111
+ end
112
+
113
+ it "has proper description for license_key" do
114
+ expect(license_key_option[:description]).to include("Chef License key")
115
+ end
116
+
117
+ it "has proper description for license_url" do
118
+ expect(license_url_option[:description]).to include("Chef License server")
119
+ end
120
+ end
121
+ end
122
+ end
@@ -59,7 +59,7 @@ describe Chef::Knife::Bootstrap do
59
59
 
60
60
  describe "when a license is not required" do
61
61
  it "does not set the chef_license" do
62
- expect(acceptor).to receive(:license_required?).and_return(false)
62
+ expect(acceptor).to receive(:license_required?).with("chef", anything).and_return(false)
63
63
  knife.check_eula_license
64
64
  expect(Chef::Config[:chef_license]).to eq(nil)
65
65
  end
@@ -67,9 +67,9 @@ describe Chef::Knife::Bootstrap do
67
67
 
68
68
  describe "when a license is required" do
69
69
  it "sets the chef_license" do
70
- expect(acceptor).to receive(:license_required?).and_return(true)
71
- expect(acceptor).to receive(:id_from_mixlib).and_return("id")
72
- expect(acceptor).to receive(:check_and_persist)
70
+ expect(acceptor).to receive(:license_required?).with("chef", anything).and_return(true)
71
+ expect(acceptor).to receive(:id_from_mixlib).with("chef").and_return("id")
72
+ expect(acceptor).to receive(:check_and_persist).with("id", anything)
73
73
  expect(acceptor).to receive(:acceptance_value).and_return("accept-no-persist")
74
74
  knife.check_eula_license
75
75
  expect(Chef::Config[:chef_license]).to eq("accept-no-persist")
@@ -100,6 +100,118 @@ describe Chef::Knife::Bootstrap do
100
100
  end
101
101
  end
102
102
 
103
+ context "#fetch_license" do
104
+ let(:licensing_config_mock) { double("ChefLicensing::Config") }
105
+ let(:licensing_handler_mock) { double("Chef::Utils::LicensingHandler") }
106
+ let(:license_mock) { double("License") }
107
+
108
+ before do
109
+ stub_const("ChefLicensing::Config", licensing_config_mock)
110
+ stub_const("Chef::Utils::LicensingHandler", licensing_handler_mock)
111
+ end
112
+
113
+ context "when bootstrap_url is provided" do
114
+ it "returns early without calling licensing" do
115
+ knife.config[:bootstrap_url] = "https://example.com/bootstrap.sh"
116
+ expect(licensing_config_mock).not_to receive(:require_license_for)
117
+
118
+ knife.send(:fetch_license)
119
+ end
120
+ end
121
+
122
+ context "when bootstrap_template is provided" do
123
+ it "returns early without calling licensing" do
124
+ knife.config[:bootstrap_template] = "custom-template"
125
+
126
+ expect(licensing_config_mock).not_to receive(:require_license_for)
127
+
128
+ knife.send(:fetch_license)
129
+ end
130
+ end
131
+
132
+ context "when both bootstrap_url and bootstrap_template are provided" do
133
+ it "returns early without calling licensing" do
134
+ knife.config[:bootstrap_url] = "https://example.com/bootstrap.sh"
135
+ knife.config[:bootstrap_template] = "custom-template"
136
+
137
+ expect(licensing_config_mock).not_to receive(:require_license_for)
138
+
139
+ knife.send(:fetch_license)
140
+ end
141
+ end
142
+
143
+ context "when neither bootstrap_url nor bootstrap_template are provided" do
144
+ before do
145
+ knife.config[:bootstrap_url] = nil
146
+ knife.config[:bootstrap_template] = nil
147
+ end
148
+
149
+ it "validates license and sets license configuration" do
150
+ expect(license_mock).to receive(:install_sh_url).and_return("https://example.com/install.sh")
151
+ expect(license_mock).to receive(:license_key).and_return("test-license-key")
152
+ expect(license_mock).to receive(:omnitruck_url).and_return("https://example.com/omnitruck")
153
+ expect(license_mock).to receive(:license_type).and_return("commercial")
154
+
155
+ expect(licensing_handler_mock).to receive(:validate!).with(no_args).and_return(license_mock)
156
+
157
+ expect(licensing_config_mock).to receive(:require_license_for).and_yield
158
+
159
+ knife.send(:fetch_license)
160
+
161
+ expect(knife.config[:license_url]).to eq("https://example.com/install.sh")
162
+ expect(knife.config[:license_id]).to eq("test-license-key")
163
+ expect(knife.config[:omnitruck_url]).to eq("https://example.com/omnitruck")
164
+ expect(knife.config[:license_type]).to eq("commercial")
165
+ end
166
+
167
+ it "handles licensing validation errors gracefully" do
168
+ expect(licensing_handler_mock).to receive(:validate!).with(no_args).and_raise(StandardError.new("License validation failed"))
169
+
170
+ expect(licensing_config_mock).to receive(:require_license_for).and_yield
171
+
172
+ expect { knife.send(:fetch_license) }.to raise_error(StandardError, "License validation failed")
173
+ end
174
+
175
+ it "properly sets all license configuration values from the license object" do
176
+ expect(license_mock).to receive(:install_sh_url).and_return("https://omnitruck.chef.io/install.sh")
177
+ expect(license_mock).to receive(:license_key).and_return("ABCD-1234-EFGH-5678")
178
+ expect(license_mock).to receive(:omnitruck_url).and_return("https://omnitruck.chef.io")
179
+ expect(license_mock).to receive(:license_type).and_return("trial")
180
+
181
+ expect(licensing_handler_mock).to receive(:validate!).with(no_args).and_return(license_mock)
182
+ expect(licensing_config_mock).to receive(:require_license_for).and_yield
183
+
184
+ # Verify that config values are nil before the call
185
+ expect(knife.config[:license_url]).to be_nil
186
+ expect(knife.config[:license_id]).to be_nil
187
+ expect(knife.config[:omnitruck_url]).to be_nil
188
+ expect(knife.config[:license_type]).to be_nil
189
+
190
+ knife.send(:fetch_license)
191
+
192
+ # Verify all license values are properly set
193
+ expect(knife.config[:license_url]).to eq("https://omnitruck.chef.io/install.sh")
194
+ expect(knife.config[:license_id]).to eq("ABCD-1234-EFGH-5678")
195
+ expect(knife.config[:omnitruck_url]).to eq("https://omnitruck.chef.io")
196
+ expect(knife.config[:license_type]).to eq("trial")
197
+ end
198
+
199
+ context "when ChefLicensing::Config.require_license_for does not yield" do
200
+ it "does not call licensing handler" do
201
+ expect(licensing_config_mock).to receive(:require_license_for).and_return(nil)
202
+ expect(licensing_handler_mock).not_to receive(:validate!)
203
+
204
+ knife.send(:fetch_license)
205
+
206
+ expect(knife.config[:license_url]).to be_nil
207
+ expect(knife.config[:license_id]).to be_nil
208
+ expect(knife.config[:omnitruck_url]).to be_nil
209
+ expect(knife.config[:license_type]).to be_nil
210
+ end
211
+ end
212
+ end
213
+ end
214
+
103
215
  context "#bootstrap_template" do
104
216
  it "should default to chef-full" do
105
217
  expect(knife.bootstrap_template).to be_a_kind_of(String)
@@ -371,6 +483,7 @@ describe Chef::Knife::Bootstrap do
371
483
  knife.merge_configs
372
484
  allow(knife).to receive(:validate_name_args!)
373
485
  expect(knife).to receive(:check_eula_license)
486
+ expect(knife).to receive(:fetch_license)
374
487
 
375
488
  expect { knife.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError)
376
489
  jsonfile.close
@@ -586,7 +699,7 @@ describe Chef::Knife::Bootstrap do
586
699
  end
587
700
 
588
701
  it "creates /etc/chef/client.d" do
589
- expect(rendered_template).to match("mkdir -p /etc/chef/client\.d")
702
+ expect(rendered_template).to match("mkdir -p /etc/chef/client.d")
590
703
  end
591
704
 
592
705
  context "a flat directory structure" do
@@ -797,7 +910,7 @@ describe Chef::Knife::Bootstrap do
797
910
  context "validating use_sudo_password option" do
798
911
  it "use_sudo_password contains description and long params for help" do
799
912
  expect(knife.options).to(have_key(:use_sudo_password)) \
800
- && expect(knife.options[:use_sudo_password][:description].to_s).not_to(eq(""))\
913
+ && expect(knife.options[:use_sudo_password][:description].to_s).not_to(eq("")) \
801
914
  && expect(knife.options[:use_sudo_password][:long].to_s).not_to(eq(""))
802
915
  end
803
916
  end
@@ -1144,7 +1257,6 @@ describe Chef::Knife::Bootstrap do
1144
1257
  before do
1145
1258
  # We will use knife's actual config since these tests
1146
1259
  # have assumptions based on CLI default values
1147
- config = {}
1148
1260
  end
1149
1261
 
1150
1262
  let(:expected_result) do
@@ -1710,6 +1822,7 @@ describe Chef::Knife::Bootstrap do
1710
1822
  describe "#run" do
1711
1823
  it "performs the steps we expect to run a bootstrap" do
1712
1824
  expect(knife).to receive(:check_eula_license)
1825
+ expect(knife).to receive(:fetch_license)
1713
1826
  expect(knife).to receive(:validate_name_args!).ordered
1714
1827
  expect(knife).to receive(:validate_protocol!).ordered
1715
1828
  expect(knife).to receive(:validate_first_boot_attributes!).ordered
@@ -1980,6 +2093,7 @@ describe Chef::Knife::Bootstrap do
1980
2093
  it "verifies that a server to bootstrap was given as a command line arg" do
1981
2094
  knife.name_args = nil
1982
2095
  expect(knife).to receive(:check_eula_license)
2096
+ expect(knife).to receive(:fetch_license)
1983
2097
  expect { knife.run }.to raise_error(SystemExit)
1984
2098
  expect(stderr.string).to match(/ERROR:.+FQDN or ip/)
1985
2099
  end