knife-ec-backup 3.0.9 → 3.0.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f2a9570f14dc2fb44eb99732fd69f1f9da32c48ceab68bd6e94511f1e2c464d
4
- data.tar.gz: de9b0f5cd3bc62c6402bd364151c1237d6e1107b89ce9b80753240091e0b3303
3
+ metadata.gz: d288abb4c07544382487a1ea61612a485fa0651e5ebb20a39db277278b900237
4
+ data.tar.gz: 21cb4ac04036f42faff0a5e93ddc3af4bed52efd8905ddf58840103d415747d6
5
5
  SHA512:
6
- metadata.gz: 5c68fe7a9f2af77f36bff226a63d131cd7dc1a9bb0dd0086b0d65125913310080b4b2acc52f1284438116aae4d14baa248624078d08b3abf37484cebacda3c5f
7
- data.tar.gz: d2d04dfab678b7d4629e88369208807be1af0f92a930dc6c2bf4d6ead3ba813836f4542ba93322c390f8e120a59c7ab9a9d07d142cb5aa3137af2578864d6e0a
6
+ metadata.gz: 022deb1b7431715bfdb5b21cf30da21bded07ae68c1cd4bbeaefeaa8b01a68cdd07bfdc335839b46f69fce64d926063e5cbfeb98fba27330319c20bc6cc591ea
7
+ data.tar.gz: c87aef9ad1f2e606d1b119ebdaa8f9ff0838a202deacf42c19f09c1849cb3657fb6650630d380cc98a70e706e7083a628ad049bd8c965dad445fdfd3938ee5c9
@@ -28,6 +28,8 @@ class Chef
28
28
  def initialize(working_dir, process)
29
29
  @err_dir = "#{working_dir}/errors"
30
30
  FileUtils.mkdir_p(@err_dir)
31
+ @error_count = 0
32
+ @process = process
31
33
 
32
34
  # Create an specific error file name depending
33
35
  # of where the process comes from.
@@ -40,7 +42,51 @@ class Chef
40
42
  end
41
43
 
42
44
  # exit handler
43
- at_exit { display(@err_file) }
45
+ at_exit do
46
+ display(@err_file)
47
+ override_exit_status
48
+ end
49
+ end
50
+
51
+ # Forces a consistent, non-zero exit status when errors were recorded so
52
+ # that a run with a high-verbosity flag (e.g. -VVV) and one without it
53
+ # report the same status. Only applies to `knife ec import`; other
54
+ # commands keep their original exit status. Skipped under RSpec so the
55
+ # test suite's own exit status is not altered.
56
+ def override_exit_status
57
+ return if defined?(RSpec)
58
+ return unless defined?(Chef::Knife::EcImport) && @process == Chef::Knife::EcImport
59
+
60
+ status = maybe_exit_status($!)
61
+ exit(status) unless status.nil?
62
+ end
63
+
64
+ # Decides the process exit status so it is identical regardless of
65
+ # verbosity. Under -VVV knife re-raises and Ruby exits 1; without it knife
66
+ # rescues and exits with a different code (e.g. 100). Both are error cases
67
+ # and must agree.
68
+ #
69
+ # +error+ is the exception propagating at exit (Ruby's $!). Returns the
70
+ # status to force, or nil to leave the current status untouched.
71
+ def maybe_exit_status(error = $!)
72
+ case error
73
+ when SystemExit
74
+ # Normalize any non-zero exit (e.g. knife's exit 100) to 1.
75
+ return 1 unless error.success?
76
+ has_errors? ? 1 : nil
77
+ when nil
78
+ # Normal termination: exit 1 only if errors were logged.
79
+ has_errors? ? 1 : nil
80
+ else
81
+ # A raw exception is propagating (typically under -VVV); Ruby already
82
+ # exits 1 and prints the backtrace, so leave it untouched.
83
+ nil
84
+ end
85
+ end
86
+
87
+ # Returns true when any error has been recorded.
88
+ def has_errors?
89
+ @error_count > 0
44
90
  end
45
91
 
46
92
  # Add an exception to the error file.
@@ -61,6 +107,7 @@ class Chef
61
107
  # The advantages of this schema is the ability to retry the backup or
62
108
  # restore and pick up where we left.
63
109
  def add(ex)
110
+ @error_count += 1
64
111
  msg = {
65
112
  timestamp: Time.now,
66
113
  message: ex.message,
@@ -31,6 +31,22 @@ class Chef
31
31
  ADMIN_GROUP_FILES = ['billing-admins.json', 'public_key_read_access.json'].freeze
32
32
  NOT_FOUND_STATUS = '404'
33
33
 
34
+ # Network/server errors that should be logged and skipped so the rest of
35
+ # the import can proceed instead of aborting. Covers 5xx "Internal Server
36
+ # Error" responses and dropped connections such as "Connection reset by
37
+ # peer".
38
+ RECOVERABLE_NETWORK_ERRORS = [
39
+ Net::HTTPClientException, # 4xx
40
+ Net::HTTPFatalError, # 5xx, e.g. Internal Server Error
41
+ Errno::ECONNRESET, # Connection reset by peer
42
+ Errno::ECONNREFUSED,
43
+ Errno::ETIMEDOUT,
44
+ ].freeze
45
+
46
+ # Substring of the RuntimeError some Ruby builds raise while computing
47
+ # cookbook checksums. It must be logged and skipped, not abort the import.
48
+ DIGEST_INHERITANCE_ERROR = 'Digest::Base cannot be directly inherited'.freeze
49
+
34
50
  option :tenant_id_header,
35
51
  :long => '--tenant-id TENANT_ID',
36
52
  :description => 'Tenant identifier added as X-Tenant-Id header for import requests'
@@ -146,13 +162,11 @@ class Chef
146
162
  def organization_exists?(orgname)
147
163
  rest.get(org_url(orgname))
148
164
  true
149
- rescue Net::HTTPClientException => ex
150
- if ex.response.code == NOT_FOUND_STATUS
151
- false
152
- else
153
- knife_ec_error_handler.add(ex)
154
- false
155
- end
165
+ rescue *RECOVERABLE_NETWORK_ERRORS => ex
166
+ return false if ex.is_a?(Net::HTTPClientException) && ex.response.code == NOT_FOUND_STATUS
167
+ ui.error "Failed to verify organization #{orgname}: #{ex.message}"
168
+ knife_ec_error_handler.add(ex)
169
+ false
156
170
  end
157
171
 
158
172
  def restore_user_acls
@@ -285,13 +299,19 @@ class Chef
285
299
  chef_fs_config.chef_fs, nil,
286
300
  config, ui,
287
301
  proc { |entry| chef_fs_config.format_path(entry) })
288
- rescue Net::HTTPClientException,
302
+ rescue *RECOVERABLE_NETWORK_ERRORS,
289
303
  Chef::ChefFS::FileSystem::NotFoundError,
290
304
  Chef::ChefFS::FileSystem::OperationFailedError,
291
305
  Chef::Exceptions::JSON::ParseError,
292
306
  JSON::ParserError => ex
293
307
  ui.error "#{pattern_str} failed to copy: #{ex.message}"
294
308
  knife_ec_error_handler.add(ex)
309
+ rescue RuntimeError => ex
310
+ # Only swallow the known Digest::Base inheritance error so the import
311
+ # continues; re-raise anything else.
312
+ raise unless ex.message.to_s.include?(DIGEST_INHERITANCE_ERROR)
313
+ ui.error "#{pattern_str} failed to copy: #{ex.message}"
314
+ knife_ec_error_handler.add(ex)
295
315
  end
296
316
 
297
317
  def sort_groups_for_upload(groups)
@@ -399,8 +419,8 @@ class Chef
399
419
 
400
420
  rest.put(cookbook_url(org_name, cookbook_name, version, 'freeze=true'),
401
421
  manifest.tap { |h| h[FROZEN_STATUS_KEY] = true })
402
- rescue Net::HTTPClientException => ex
403
- ui.warn "Failed to freeze cookbook #{cookbook_name} #{version}: #{ex.message}"
422
+ rescue *RECOVERABLE_NETWORK_ERRORS => ex
423
+ ui.error "Failed to freeze cookbook #{cookbook_name} #{version}: #{ex.message}"
404
424
  knife_ec_error_handler.add(ex)
405
425
  end
406
426
 
@@ -414,7 +434,8 @@ class Chef
414
434
  rest.put("#{url}/#{permission}", { permission => acls[permission] })
415
435
  end
416
436
  end
417
- rescue Net::HTTPClientException => ex
437
+ rescue *RECOVERABLE_NETWORK_ERRORS => ex
438
+ ui.error "Failed to update ACL #{url}: #{ex.message}"
418
439
  knife_ec_error_handler.add(ex)
419
440
  end
420
441
  end
@@ -1,4 +1,4 @@
1
1
  # when you change this to double quotes, also update .expeditor/update_version.sh
2
2
  module KnifeECBackup
3
- VERSION = '3.0.9'
3
+ VERSION = '3.0.11'
4
4
  end
@@ -115,4 +115,47 @@ EOF
115
115
  @knife_ec_error_handler.add(cheffs_filesystem_exception('OperationFailedError'))
116
116
  expect(File.read(err_file)).to match mock_content.strip
117
117
  end
118
+
119
+ describe "#has_errors?" do
120
+ it "is false before any error is added" do
121
+ expect(@knife_ec_error_handler.has_errors?).to be false
122
+ end
123
+
124
+ it "is true after an error is added" do
125
+ @knife_ec_error_handler.add(net_exception(500))
126
+ expect(@knife_ec_error_handler.has_errors?).to be true
127
+ end
128
+ end
129
+
130
+ describe "#maybe_exit_status" do
131
+ context "when no error was recorded" do
132
+ it "leaves a clean exit untouched" do
133
+ expect(@knife_ec_error_handler.maybe_exit_status(nil)).to be_nil
134
+ end
135
+
136
+ it "normalizes knife's non-zero exit (e.g. 100) to 1" do
137
+ expect(@knife_ec_error_handler.maybe_exit_status(SystemExit.new(100))).to eq 1
138
+ end
139
+
140
+ it "leaves a clean SystemExit untouched" do
141
+ expect(@knife_ec_error_handler.maybe_exit_status(SystemExit.new(0))).to be_nil
142
+ end
143
+
144
+ it "leaves a raw exception (e.g. under -VVV) untouched" do
145
+ expect(@knife_ec_error_handler.maybe_exit_status(RuntimeError.new('boom'))).to be_nil
146
+ end
147
+ end
148
+
149
+ context "when an error was recorded" do
150
+ before(:each) { @knife_ec_error_handler.add(net_exception(500)) }
151
+
152
+ it "forces exit 1 on an otherwise clean exit" do
153
+ expect(@knife_ec_error_handler.maybe_exit_status(nil)).to eq 1
154
+ end
155
+
156
+ it "normalizes knife's exit 100 to 1" do
157
+ expect(@knife_ec_error_handler.maybe_exit_status(SystemExit.new(100))).to eq 1
158
+ end
159
+ end
160
+ end
118
161
  end
@@ -463,6 +463,7 @@ describe Chef::Knife::EcImport do
463
463
  it HANDLES_ERRORS do
464
464
  allow(@rest).to receive(:get).and_raise(import_net_exception(500))
465
465
  expect(@error_handler).to receive(:add)
466
+ expect(@knife.ui).to receive(:error).with(/Failed to update ACL/)
466
467
  @knife.put_acl(@rest, "url", {})
467
468
  end
468
469
  end
@@ -562,7 +563,7 @@ describe Chef::Knife::EcImport do
562
563
  it HANDLES_ERRORS do
563
564
  allow(@rest).to receive(:get).and_raise(import_net_exception(500))
564
565
  expect(@error_handler).to receive(:add)
565
- expect(@knife.ui).to receive(:warn).with(/Failed to freeze cookbook/)
566
+ expect(@knife.ui).to receive(:error).with(/Failed to freeze cookbook/)
566
567
  @knife.freeze_cookbook("mycb", "1.0.0", "foo")
567
568
  end
568
569
  end
@@ -598,6 +599,16 @@ describe Chef::Knife::EcImport do
598
599
  expect(@error_handler).to receive(:add)
599
600
  @knife.chef_fs_copy_pattern("/foo", chef_fs_config)
600
601
  end
602
+
603
+ [Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT].each do |socket_error|
604
+ it "recovers from #{socket_error} socket failures" do
605
+ chef_fs_config = double("config", :local_fs => double, :chef_fs => double)
606
+ allow(Chef::ChefFS::FileSystem).to receive(:copy_to).and_raise(socket_error)
607
+ expect(@knife.ui).to receive(:error).with(/\/foo failed to copy:/)
608
+ expect(@error_handler).to receive(:add)
609
+ @knife.chef_fs_copy_pattern("/foo", chef_fs_config)
610
+ end
611
+ end
601
612
  end
602
613
 
603
614
  describe "#sort_groups_for_upload" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: knife-ec-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.9
4
+ version: 3.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Keiser
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-30 00:00:00.000000000 Z
11
+ date: 2026-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -119,7 +119,7 @@ homepage: https://www.chef.io
119
119
  licenses:
120
120
  - Apache-2.0
121
121
  metadata: {}
122
- post_install_message:
122
+ post_install_message:
123
123
  rdoc_options: []
124
124
  require_paths:
125
125
  - lib
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
135
  version: '0'
136
136
  requirements: []
137
137
  rubygems_version: 3.3.27
138
- signing_key:
138
+ signing_key:
139
139
  specification_version: 4
140
140
  summary: Backup and Restore of Enterprise Chef
141
141
  test_files: []