hetzner-k3s 0.4.9 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22358cdc272faa5e09ae2f561bf4225990a87b0461f4920439f9d5e9543fbe59
4
- data.tar.gz: fc7dc822d53cd881e01a18c509d666bdd0321ed25c636f2873bca3fa1e5e0ce9
3
+ metadata.gz: 3855e58a70b2b16e6ae421669ad22031bc31a66df36aea8ea31d42b060e7192c
4
+ data.tar.gz: '09042dc486c0bf330ca9d5df2407ae13fac7f7311c4cb3314651b675ffa8c49c'
5
5
  SHA512:
6
- metadata.gz: 35a06d127f14f4848a6a87611292b67e0edbf6cc3fb1ae8b803214428369cfc519942702fe36c14c2537732b38fa024b5d2361f77db56ea58724446f4822537d
7
- data.tar.gz: bfc7751afa7db09a5e929b8164cdab060dfcb7b86125ae30a4804e179ba8a0550bdb07c9a181e148284e10ef7a632fe26ad81da1e278aeeab492a2899e2cdffb
6
+ metadata.gz: 705761dcb4bd361c3f417f44cb3f44d2ae4a8941ce822cec083537f1b24eeb66d15a1e840290b4432d6bd715ccfc5626bf4aa3d811e47772be4775dc9347618f
7
+ data.tar.gz: c945bc3428e87c465f05a90d7b3b8b8d2ec3ed9f9da55d471a758a287230bbb33074f783bb96a06d36ecc204a41b5b6793044e86df98e2fd1f7c6be0d5549c56
data/.rubocop.yml ADDED
@@ -0,0 +1,121 @@
1
+ Gemspec/DateAssignment: # new in 1.10
2
+ Enabled: true
3
+ Gemspec/RequireMFA: # new in 1.23
4
+ Enabled: true
5
+ Layout/LineEndStringConcatenationIndentation: # new in 1.18
6
+ Enabled: true
7
+ Layout/SpaceBeforeBrackets: # new in 1.7
8
+ Enabled: true
9
+ Lint/AmbiguousAssignment: # new in 1.7
10
+ Enabled: true
11
+ Lint/AmbiguousOperatorPrecedence: # new in 1.21
12
+ Enabled: true
13
+ Lint/AmbiguousRange: # new in 1.19
14
+ Enabled: true
15
+ Lint/DeprecatedConstants: # new in 1.8
16
+ Enabled: true
17
+ Lint/DuplicateBranch: # new in 1.3
18
+ Enabled: true
19
+ Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
20
+ Enabled: true
21
+ Lint/EmptyBlock: # new in 1.1
22
+ Enabled: true
23
+ Lint/EmptyClass: # new in 1.3
24
+ Enabled: true
25
+ Lint/EmptyInPattern: # new in 1.16
26
+ Enabled: true
27
+ Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
28
+ Enabled: true
29
+ Lint/LambdaWithoutLiteralBlock: # new in 1.8
30
+ Enabled: true
31
+ Lint/NoReturnInBeginEndBlocks: # new in 1.2
32
+ Enabled: true
33
+ Lint/NumberedParameterAssignment: # new in 1.9
34
+ Enabled: true
35
+ Lint/OrAssignmentToConstant: # new in 1.9
36
+ Enabled: true
37
+ Lint/RedundantDirGlobSort: # new in 1.8
38
+ Enabled: true
39
+ Lint/RequireRelativeSelfPath: # new in 1.22
40
+ Enabled: true
41
+ Lint/SymbolConversion: # new in 1.9
42
+ Enabled: true
43
+ Lint/ToEnumArguments: # new in 1.1
44
+ Enabled: true
45
+ Lint/TripleQuotes: # new in 1.9
46
+ Enabled: true
47
+ Lint/UnexpectedBlockArity: # new in 1.5
48
+ Enabled: true
49
+ Lint/UnmodifiedReduceAccumulator: # new in 1.1
50
+ Enabled: true
51
+ Lint/UselessRuby2Keywords: # new in 1.23
52
+ Enabled: true
53
+ Naming/BlockForwarding: # new in 1.24
54
+ Enabled: true
55
+ Security/IoMethods: # new in 1.22
56
+ Enabled: true
57
+ Style/ArgumentsForwarding: # new in 1.1
58
+ Enabled: true
59
+ Style/CollectionCompact: # new in 1.2
60
+ Enabled: true
61
+ Style/DocumentDynamicEvalDefinition: # new in 1.1
62
+ Enabled: true
63
+ Style/EndlessMethod: # new in 1.8
64
+ Enabled: true
65
+ Style/FileRead: # new in 1.24
66
+ Enabled: true
67
+ Style/FileWrite: # new in 1.24
68
+ Enabled: true
69
+ Style/HashConversion: # new in 1.10
70
+ Enabled: true
71
+ Style/HashExcept: # new in 1.7
72
+ Enabled: true
73
+ Style/IfWithBooleanLiteralBranches: # new in 1.9
74
+ Enabled: true
75
+ Style/InPatternThen: # new in 1.16
76
+ Enabled: true
77
+ Style/MapToHash: # new in 1.24
78
+ Enabled: true
79
+ Style/MultilineInPatternThen: # new in 1.16
80
+ Enabled: true
81
+ Style/NegatedIfElseCondition: # new in 1.2
82
+ Enabled: true
83
+ Style/NilLambda: # new in 1.3
84
+ Enabled: true
85
+ Style/NumberedParameters: # new in 1.22
86
+ Enabled: true
87
+ Style/NumberedParametersLimit: # new in 1.22
88
+ Enabled: true
89
+ Style/OpenStructUse: # new in 1.23
90
+ Enabled: true
91
+ Style/QuotedSymbols: # new in 1.16
92
+ Enabled: true
93
+ Style/RedundantArgument: # new in 1.4
94
+ Enabled: true
95
+ Style/RedundantSelfAssignmentBranch: # new in 1.19
96
+ Enabled: true
97
+ Style/SelectByRegexp: # new in 1.22
98
+ Enabled: true
99
+ Style/StringChars: # new in 1.12
100
+ Enabled: true
101
+ Style/SwapValues: # new in 1.1
102
+ Enabled: true
103
+ Style/Documentation:
104
+ Enabled: false
105
+ Metrics/MethodLength:
106
+ Enabled: false
107
+ Metrics/AbcSize:
108
+ Enabled: false
109
+ Metrics/CyclomaticComplexity:
110
+ Enabled: false
111
+ Metrics/ClassLength:
112
+ Enabled: false
113
+ Layout/LineLength:
114
+ Enabled: false
115
+ Metrics/PerceivedComplexity:
116
+ Enabled: false
117
+ Metrics/ParameterLists:
118
+ Max: 10
119
+ Style/FrozenStringLiteralComment:
120
+ Exclude:
121
+ - exe/hetzner-k3s
data/Dockerfile CHANGED
@@ -1,7 +1,10 @@
1
- FROM ruby:2.7.4-alpine
1
+ FROM ruby:3.1.0-alpine
2
2
 
3
3
  RUN apk update --no-cache \
4
- && apk add build-base git openssh-client
4
+ && apk add build-base git openssh-client curl bash
5
+
6
+ RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
7
+ && install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
5
8
 
6
9
  COPY . .
7
10
 
data/Gemfile CHANGED
@@ -1,7 +1,9 @@
1
- source "https://rubygems.org"
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in k3s.gemspec
4
6
  gemspec
5
7
 
6
- gem "rake", "~> 12.0"
7
- gem "rspec", "~> 3.0"
8
+ gem 'rake', '~> 12.0'
9
+ gem 'rspec', '~> 3.0'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hetzner-k3s (0.4.8)
4
+ hetzner-k3s (0.5.3)
5
5
  bcrypt_pbkdf
6
6
  ed25519
7
7
  http
@@ -15,12 +15,13 @@ GEM
15
15
  specs:
16
16
  addressable (2.8.0)
17
17
  public_suffix (>= 2.0.2, < 5.0)
18
+ ast (2.4.2)
18
19
  bcrypt_pbkdf (1.1.0)
19
20
  diff-lcs (1.4.4)
20
21
  domain_name (0.5.20190701)
21
22
  unf (>= 0.0.5, < 1.0.0)
22
- ed25519 (1.2.4)
23
- ffi (1.15.4)
23
+ ed25519 (1.3.0)
24
+ ffi (1.15.5)
24
25
  ffi-compiler (1.0.1)
25
26
  ffi (>= 1.0.0)
26
27
  rake
@@ -35,8 +36,14 @@ GEM
35
36
  http-parser (1.2.3)
36
37
  ffi-compiler (>= 1.0, < 2.0)
37
38
  net-ssh (6.1.0)
39
+ parallel (1.21.0)
40
+ parser (3.1.0.0)
41
+ ast (~> 2.4.1)
38
42
  public_suffix (4.0.6)
43
+ rainbow (3.1.1)
39
44
  rake (12.3.3)
45
+ regexp_parser (2.2.0)
46
+ rexml (3.2.5)
40
47
  rspec (3.10.0)
41
48
  rspec-core (~> 3.10.0)
42
49
  rspec-expectations (~> 3.10.0)
@@ -50,12 +57,25 @@ GEM
50
57
  diff-lcs (>= 1.2.0, < 2.0)
51
58
  rspec-support (~> 3.10.0)
52
59
  rspec-support (3.10.2)
60
+ rubocop (1.25.1)
61
+ parallel (~> 1.10)
62
+ parser (>= 3.1.0.0)
63
+ rainbow (>= 2.2.2, < 4.0)
64
+ regexp_parser (>= 1.8, < 3.0)
65
+ rexml
66
+ rubocop-ast (>= 1.15.1, < 2.0)
67
+ ruby-progressbar (~> 1.7)
68
+ unicode-display_width (>= 1.4.0, < 3.0)
69
+ rubocop-ast (1.15.1)
70
+ parser (>= 3.0.1.1)
71
+ ruby-progressbar (1.11.0)
53
72
  sshkey (2.0.0)
54
73
  subprocess (1.5.5)
55
74
  thor (1.2.1)
56
75
  unf (0.1.4)
57
76
  unf_ext
58
77
  unf_ext (0.0.8)
78
+ unicode-display_width (2.1.0)
59
79
 
60
80
  PLATFORMS
61
81
  ruby
@@ -64,6 +84,7 @@ DEPENDENCIES
64
84
  hetzner-k3s!
65
85
  rake (~> 12.0)
66
86
  rspec (~> 3.0)
87
+ rubocop
67
88
 
68
89
  BUNDLED WITH
69
90
  2.3.4
data/README.md CHANGED
@@ -14,6 +14,7 @@ Using this tool, creating a highly available k3s cluster with 3 masters for the
14
14
  - installing the [Hetzner CSI Driver](https://github.com/hetznercloud/csi-driver) to provision persistent volumes using Hetzner's block storage
15
15
  - installing the [Rancher System Upgrade Controller](https://github.com/rancher/system-upgrade-controller) to make upgrades to a newer version of k3s easy and quick
16
16
 
17
+ See roadmap [here](https://github.com/vitobotta/hetzner-k3s/projects/1) for the features planned or in progress.
17
18
 
18
19
  ## Requirements
19
20
 
@@ -25,7 +26,7 @@ All that is needed to use this tool is
25
26
 
26
27
  ## Installation
27
28
 
28
- Once you have the Ruby runtime up and running (2.7.2 or newer in the 2.7 series is recommended at this stage), you just need to install the gem:
29
+ Once you have the Ruby runtime up and running (3.1.0 or newer), you just need to install the gem:
29
30
 
30
31
  ```bash
31
32
  gem install hetzner-k3s
@@ -38,7 +39,7 @@ This will install the `hetzner-k3s` executable in your PATH.
38
39
  Alternatively, if you don't want to set up a Ruby runtime but have Docker installed, you can use a container. Run the following from inside the directory where you have the config file for the cluster (described in the next section):
39
40
 
40
41
  ```bash
41
- docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.4.9 create-cluster --config-file /cluster/test.yaml
42
+ docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.5.3 create-cluster --config-file /cluster/test.yaml
42
43
  ```
43
44
 
44
45
  Replace `test.yaml` with the name of your config file.
@@ -70,11 +71,14 @@ worker_node_pools:
70
71
  - name: big
71
72
  instance_type: cpx31
72
73
  instance_count: 2
74
+ additional_packages:
75
+ - somepackage
76
+ enable_encryption: true
73
77
  ```
74
78
 
75
79
  It should hopefully be self explanatory; you can run `hetzner-k3s releases` to see a list of the available releases from the most recent to the oldest available.
76
80
 
77
- If you are using Docker, then set `kubeconfig_path` to `/cluster/kubeconfig` so that the kubeconfig is created in the same directory where your config file is.
81
+ If you are using Docker, then set `kubeconfig_path` to `/cluster/kubeconfig` so that the kubeconfig is created in the same directory where your config file is. Also set the config file path to `/cluster/<filename>`.
78
82
 
79
83
  If you don't want to specify the Hetzner token in the config file (for example if you want to use the tool with CI), then you can use the `HCLOUD_TOKEN` environment variable instead, which has predecence.
80
84
 
@@ -252,88 +256,6 @@ Once the cluster is ready you can create persistent volumes out of the box with
252
256
  I recommend that you create a separate Hetzner project for each cluster, because otherwise multiple clusters will attempt to create overlapping routes. I will make the pod cidr configurable in the future to avoid this, but I still recommend keeping clusters separated from each other. This way, if you want to delete a cluster with all the resources created for it, you can just delete the project.
253
257
 
254
258
 
255
- ## changelog
256
-
257
- - 0.4.9
258
- - Ensure the program always exits with exit code 1 if the config file fails validation
259
- - Upgrade System Upgrade Controller to 0.8.1
260
- - Remove dependency on unmaintained gem k8s-ruby
261
- - Make the gem compatible with Ruby 3.1.0
262
-
263
- - 0.4.8
264
- - Increase timeout with API requests to 30 seconds
265
- - Limit number of retries for API requests to 3
266
- - Ensure all version tags are listed for k3s (thanks @janosmiko)
267
-
268
- - 0.4.7
269
- - Made it possible to specify a custom image/snapshot for the servers
270
-
271
- - 0.4.6
272
- - Added a check to abort gracefully when for some reason one or more servers are not created, for example due to temporary problems with the Hetzner API.
273
-
274
- - 0.4.5
275
- - Fix network creation (bug introduced in the previous version)
276
-
277
- - 0.4.4
278
- - Add support for the new Ashburn, Virginia (USA) location
279
- - Automatically use a placement group so that the instances are all created on different physical hosts for high availability
280
-
281
- - 0.4.3
282
- - Fix an issue with SSH key creation
283
-
284
- - 0.4.2
285
- - Update Hetzner CSI driver to v1.6.0
286
- - Update System Upgrade Controller to v0.8.0
287
-
288
- - 0.4.1
289
- - Allow to optionally specify the path of the private SSH key
290
- - Set correct permissions for the kubeconfig file
291
- - Retry fetching manifests a few times to allow for temporary network issues
292
- - Allow to optionally schedule workloads on masters
293
- - Allow clusters with no worker node pools if scheduling is enabled for the masters
294
-
295
- - 0.4.0
296
- - Ensure the masters are removed from the API load balancer before deleting the load balancer
297
- - Ensure the servers are removed from the firewall before deleting it
298
- - Allow using an environment variable to specify the Hetzner token
299
- - Allow restricting SSH access to the nodes to specific networks
300
- - Do not open the port 6443 on the nodes if a load balancer is created for an HA cluster
301
-
302
- - 0.3.9
303
- - Add command "version" to print the version of the tool in use
304
-
305
- - 0.3.8
306
- - Fix: added a check on a label to ensure that only servers that belong to the cluster are deleted from the project
307
-
308
- - 0.3.7
309
- - Ensure that the cluster name only contains lowercase letters, digits and dashes for compatibility with the cloud controller manager
310
-
311
- - 0.3.6
312
- - Retry SSH commands when IO errors occur
313
-
314
- - 0.3.5
315
- - Add descriptions for firewall rules
316
-
317
- - 0.3.4
318
- - Added Docker support
319
-
320
- - 0.3.3
321
- - Add some gems required on Linux
322
-
323
- - 0.3.2
324
- - Configure DNS to use Cloudflare's resolver instead of Hetzner's, since Hetzner's resolvers are not always reliable
325
-
326
- - 0.3.1
327
- - Allow enabling/disabling the host key verification
328
-
329
- - 0.3.0
330
- - Handle case when an SSH key with the given fingerprint already exists in the Hetzner project
331
- - Handle a timeout of 5 seconds for requests to the Hetzner API
332
- - Retry waiting for server to be up when timeouts/host-unreachable errors occur
333
- - Ignore known_hosts entry to prevent errors when recreating servers with IPs that have been used previously
334
-
335
- - 0.2.0
336
- - Allow mixing servers of different series Intel/AMD
337
259
  ## Contributing and support
338
260
 
339
261
  Please create a PR if you want to propose any changes, or open an issue if you are having trouble with the tool - I will do my best to help if I can.
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
data/bin/build.sh CHANGED
@@ -6,9 +6,9 @@ set -e
6
6
 
7
7
  IMAGE="vitobotta/hetzner-k3s"
8
8
 
9
- docker build -t ${IMAGE}:v9 \
9
+ docker build -t ${IMAGE}:v0.5.3 \
10
10
  --platform=linux/amd64 \
11
- --cache-from ${IMAGE}:v0.4.8 \
11
+ --cache-from ${IMAGE}:v0.5.2 \
12
12
  --build-arg BUILDKIT_INLINE_CACHE=1 .
13
13
 
14
- docker push vitobotta/hetzner-k3s:v0.4.9
14
+ docker push vitobotta/hetzner-k3s:v0.5.3
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "k3s"
3
+ require 'bundler/setup'
4
+ require 'k3s'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "k3s"
10
10
  # require "pry"
11
11
  # Pry.start
12
12
 
13
- require "irb"
13
+ require 'irb'
14
14
  IRB.start(__FILE__)
File without changes
data/hetzner-k3s.gemspec CHANGED
@@ -1,37 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'lib/hetzner/k3s/version'
2
4
 
3
5
  Gem::Specification.new do |spec|
4
- spec.name = "hetzner-k3s"
6
+ spec.name = 'hetzner-k3s'
5
7
  spec.version = Hetzner::K3s::VERSION
6
- spec.authors = ["Vito Botta"]
7
- spec.email = ["vito@botta.me"]
8
+ spec.authors = ['Vito Botta']
9
+ spec.email = ['vito@botta.me']
8
10
 
9
- spec.summary = %q{A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using k3s.}
10
- spec.description = %q{A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using k3s.}
11
- spec.homepage = "https://github.com/vitobotta/hetzner-k3s"
12
- spec.license = "MIT"
13
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
11
+ spec.summary = 'A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using k3s.'
12
+ spec.description = 'A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using k3s.'
13
+ spec.homepage = 'https://github.com/vitobotta/hetzner-k3s'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
14
16
 
15
17
  # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
18
 
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/vitobotta/hetzner-k3s"
19
- spec.metadata["changelog_uri"] = "https://github.com/vitobotta/hetzner-k3s"
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/vitobotta/hetzner-k3s'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/vitobotta/hetzner-k3s'
20
22
 
21
- spec.add_dependency "thor"
22
- spec.add_dependency "http"
23
- spec.add_dependency "net-ssh"
24
- spec.add_dependency "sshkey"
25
- spec.add_dependency "ed25519"
26
- spec.add_dependency "bcrypt_pbkdf"
27
- spec.add_dependency "subprocess"
23
+ spec.add_dependency 'bcrypt_pbkdf'
24
+ spec.add_dependency 'ed25519'
25
+ spec.add_dependency 'http'
26
+ spec.add_dependency 'net-ssh'
27
+ spec.add_dependency 'sshkey'
28
+ spec.add_dependency 'subprocess'
29
+ spec.add_dependency 'thor'
30
+ spec.add_development_dependency 'rubocop'
28
31
 
29
32
  # Specify which files should be added to the gem when it is released.
30
33
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
34
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
32
35
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
36
  end
34
- spec.bindir = "exe"
37
+ spec.bindir = 'exe'
35
38
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
- spec.require_paths = ["lib"]
39
+ spec.require_paths = ['lib']
40
+ spec.metadata['rubygems_mfa_required'] = 'true'
37
41
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hetzner
2
4
  class Client
3
- BASE_URI = "https://api.hetzner.cloud/v1"
5
+ BASE_URI = 'https://api.hetzner.cloud/v1'
4
6
 
5
7
  attr_reader :token
6
8
 
@@ -22,27 +24,27 @@ module Hetzner
22
24
 
23
25
  def delete(path, id)
24
26
  make_request do
25
- HTTP.headers(headers).delete(BASE_URI + path + "/" + id.to_s)
27
+ HTTP.headers(headers).delete("#{BASE_URI}#{path}/#{id}")
26
28
  end
27
29
  end
28
30
 
29
31
  private
30
32
 
31
- def headers
32
- {
33
- "Authorization": "Bearer #{@token}",
34
- "Content-Type": "application/json"
35
- }
36
- end
33
+ def headers
34
+ {
35
+ Authorization: "Bearer #{@token}",
36
+ 'Content-Type': 'application/json'
37
+ }
38
+ end
37
39
 
38
- def make_request &block
39
- retries ||= 0
40
+ def make_request(&block)
41
+ retries ||= 0
40
42
 
41
- Timeout::timeout(30) do
42
- block.call
43
- end
44
- rescue Timeout::Error
45
- retry if (retries += 1) < 3
43
+ Timeout.timeout(30) do
44
+ block.call
46
45
  end
46
+ rescue Timeout::Error
47
+ retry if (retries += 1) < 3
48
+ end
47
49
  end
48
50
  end