hetzner-k3s 0.4.6 → 0.5.0

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: 60167b5a135a2932bff53b3d030b22940e3a7a943584ab9645d420fdfa2f59f4
4
- data.tar.gz: 6b36f71696ffb4f94d1dfde94b5215c46b9f92fc482c0da148ea84c0cfa5a56f
3
+ metadata.gz: 21205ebaea746e26fa42437afe5feca2c9fb856861e6eebedd0608b023d0addf
4
+ data.tar.gz: e67f315b4b96e98fab4fc514e1a01ab88e79b701c83177e0c53863150e508d10
5
5
  SHA512:
6
- metadata.gz: 73242160d8701cfb92332bf18aefcf7e2c15275493b2e4dedfc16d1e577367e91e338b2c57fc3d5731a5f68317751e1644b7d2be4d505371ee1543f04365685f
7
- data.tar.gz: 2da1c1ef1bf32b3c403868d47bc94fbeee1ad8479aa1272db9141c749b695e70489c85c19f8e891fd332a91de6deda000d273e959f1d1d6a1e88edf9efe4310f
6
+ metadata.gz: b2bf2e628f2326e63c10ebe076882979913f260a80c8b01b54944ef226341bf5cbe091283b168af566e6eda13ad84ad9742832284b28973ceb96ea87d75c911b
7
+ data.tar.gz: d5c21906a5eb59772d7613a821bb76a451e95e6f3b4f5b39321c1c431c27de5451c6fc890e37e8622077aab8c54669132227fbfd4622c85cc9838c5bd616c2b1
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.0
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:2.7.4-alpine
1
+ FROM ruby:3.1.0-alpine
2
2
 
3
3
  RUN apk update --no-cache \
4
4
  && apk add build-base git openssh-client
data/Gemfile.lock CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hetzner-k3s (0.4.5)
4
+ hetzner-k3s (0.4.8)
5
5
  bcrypt_pbkdf
6
6
  ed25519
7
7
  http
8
- k8s-ruby
9
8
  net-ssh
10
9
  sshkey
10
+ subprocess
11
11
  thor
12
12
 
13
13
  GEM
@@ -16,43 +16,14 @@ GEM
16
16
  addressable (2.8.0)
17
17
  public_suffix (>= 2.0.2, < 5.0)
18
18
  bcrypt_pbkdf (1.1.0)
19
- concurrent-ruby (1.1.9)
20
19
  diff-lcs (1.4.4)
21
20
  domain_name (0.5.20190701)
22
21
  unf (>= 0.0.5, < 1.0.0)
23
- dry-configurable (0.13.0)
24
- concurrent-ruby (~> 1.0)
25
- dry-core (~> 0.6)
26
- dry-container (0.9.0)
27
- concurrent-ruby (~> 1.0)
28
- dry-configurable (~> 0.13, >= 0.13.0)
29
- dry-core (0.7.1)
30
- concurrent-ruby (~> 1.0)
31
- dry-equalizer (0.3.0)
32
- dry-inflector (0.2.1)
33
- dry-logic (0.6.1)
34
- concurrent-ruby (~> 1.0)
35
- dry-core (~> 0.2)
36
- dry-equalizer (~> 0.2)
37
- dry-struct (0.5.1)
38
- dry-core (~> 0.4, >= 0.4.3)
39
- dry-equalizer (~> 0.2)
40
- dry-types (~> 0.13)
41
- ice_nine (~> 0.11)
42
- dry-types (0.13.4)
43
- concurrent-ruby (~> 1.0)
44
- dry-container (~> 0.3)
45
- dry-core (~> 0.4, >= 0.4.4)
46
- dry-equalizer (~> 0.2)
47
- dry-inflector (~> 0.1, >= 0.1.2)
48
- dry-logic (~> 0.4, >= 0.4.2)
49
22
  ed25519 (1.2.4)
50
- excon (0.85.0)
51
- ffi (1.15.3)
23
+ ffi (1.15.4)
52
24
  ffi-compiler (1.0.1)
53
25
  ffi (>= 1.0.0)
54
26
  rake
55
- hashdiff (1.0.1)
56
27
  http (4.4.1)
57
28
  addressable (~> 2.3)
58
29
  http-cookie (~> 1.0)
@@ -63,24 +34,9 @@ GEM
63
34
  http-form_data (2.3.0)
64
35
  http-parser (1.2.3)
65
36
  ffi-compiler (>= 1.0, < 2.0)
66
- ice_nine (0.11.2)
67
- jsonpath (0.9.9)
68
- multi_json
69
- to_regexp (~> 0.2.1)
70
- k8s-ruby (0.10.5)
71
- dry-struct (~> 0.5.0)
72
- dry-types (~> 0.13.0)
73
- excon (~> 0.71)
74
- hashdiff (~> 1.0.0)
75
- jsonpath (~> 0.9.5)
76
- recursive-open-struct (~> 1.1.0)
77
- yajl-ruby (~> 1.4.0)
78
- yaml-safe_load_stream (~> 0.1)
79
- multi_json (1.15.0)
80
37
  net-ssh (6.1.0)
81
38
  public_suffix (4.0.6)
82
39
  rake (12.3.3)
83
- recursive-open-struct (1.1.3)
84
40
  rspec (3.10.0)
85
41
  rspec-core (~> 3.10.0)
86
42
  rspec-expectations (~> 3.10.0)
@@ -95,13 +51,11 @@ GEM
95
51
  rspec-support (~> 3.10.0)
96
52
  rspec-support (3.10.2)
97
53
  sshkey (2.0.0)
98
- thor (1.1.0)
99
- to_regexp (0.2.1)
54
+ subprocess (1.5.5)
55
+ thor (1.2.1)
100
56
  unf (0.1.4)
101
57
  unf_ext
102
58
  unf_ext (0.0.8)
103
- yajl-ruby (1.4.1)
104
- yaml-safe_load_stream (0.1.1)
105
59
 
106
60
  PLATFORMS
107
61
  ruby
@@ -112,4 +66,4 @@ DEPENDENCIES
112
66
  rspec (~> 3.0)
113
67
 
114
68
  BUNDLED WITH
115
- 2.1.4
69
+ 2.3.4
data/README.md CHANGED
@@ -38,7 +38,7 @@ This will install the `hetzner-k3s` executable in your PATH.
38
38
  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
39
 
40
40
  ```bash
41
- docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.4.6 create-cluster --config-file /cluster/test.yaml
41
+ docker run --rm -it -v ${PWD}:/cluster -v ${HOME}/.ssh:/tmp/.ssh vitobotta/hetzner-k3s:v0.5.0 create-cluster --config-file /cluster/test.yaml
42
42
  ```
43
43
 
44
44
  Replace `test.yaml` with the name of your config file.
@@ -70,6 +70,9 @@ worker_node_pools:
70
70
  - name: big
71
71
  instance_type: cpx31
72
72
  instance_count: 2
73
+ additional_packages:
74
+ - somepackage
75
+ enable_ipsec_encryption: true
73
76
  ```
74
77
 
75
78
  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.
@@ -97,8 +100,20 @@ curl \
97
100
  'https://api.hetzner.cloud/v1/server_types'
98
101
  ```
99
102
 
103
+ By default, the image in use is Ubuntu 20.04, but you can specify an image to use with the `image` config option. This makes it also possible
104
+ to use a snapshot that you have already created from and existing server (for example to preinstall some tools). If you want to use a custom
105
+ snapshot you'll need to specify the **ID** of the snapshot/image, not the description you gave when you created the template server. To find
106
+ the ID of your custom image/snapshot, run:
100
107
 
101
- Note: the option `verify_host_key` is by default set to `false` to disable host key verification. This is because sometimes when creating new servers, Hetzner may assign IP addresses that were previously used by other servers you owned in the past. Therefore the host key verification would fail. If you set this option to `true` and this happens, the tool won't be able to continue creating the cluster until you resolve the issue with one of the suggestions it will give you.
108
+ ```bash
109
+ curl \
110
+ -H "Authorization: Bearer $API_TOKEN" \
111
+ 'https://api.hetzner.cloud/v1/images'
112
+ ```
113
+
114
+ Note that if you use a custom image, the creation of the servers may take longer than when using the default image.
115
+
116
+ Also note: the option `verify_host_key` is by default set to `false` to disable host key verification. This is because sometimes when creating new servers, Hetzner may assign IP addresses that were previously used by other servers you owned in the past. Therefore the host key verification would fail. If you set this option to `true` and this happens, the tool won't be able to continue creating the cluster until you resolve the issue with one of the suggestions it will give you.
102
117
 
103
118
  Finally, to create the cluster run:
104
119
 
@@ -199,7 +214,7 @@ kubectl label node <master1> <master2> <master2> plan.upgrade.cattle.io/k3s-serv
199
214
  To delete a cluster, running
200
215
 
201
216
  ```bash
202
- hetzner-k3s delete-cluster --config-file cluster_config.yam
217
+ hetzner-k3s delete-cluster --config-file cluster_config.yaml
203
218
  ```
204
219
 
205
220
  This will delete all the resources in the Hetzner Cloud project for the cluster being deleted.
@@ -242,7 +257,25 @@ I recommend that you create a separate Hetzner project for each cluster, because
242
257
 
243
258
  ## changelog
244
259
 
245
- - 0.4.5
260
+ - 0.5.0
261
+ - Allow installing additional packages when creating the servers
262
+ - Allow enabling ipsec encryption
263
+
264
+ - 0.4.9
265
+ - Ensure the program always exits with exit code 1 if the config file fails validation
266
+ - Upgrade System Upgrade Controller to 0.8.1
267
+ - Remove dependency on unmaintained gem k8s-ruby
268
+ - Make the gem compatible with Ruby 3.1.0
269
+
270
+ - 0.4.8
271
+ - Increase timeout with API requests to 30 seconds
272
+ - Limit number of retries for API requests to 3
273
+ - Ensure all version tags are listed for k3s (thanks @janosmiko)
274
+
275
+ - 0.4.7
276
+ - Made it possible to specify a custom image/snapshot for the servers
277
+
278
+ - 0.4.6
246
279
  - 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.
247
280
 
248
281
  - 0.4.5
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}:v0.4.6 \
9
+ docker build -t ${IMAGE}:v0.5.0 \
10
10
  --platform=linux/amd64 \
11
- --cache-from ${IMAGE}:v0.4.5 \
11
+ --cache-from ${IMAGE}:v0.4.9 \
12
12
  --build-arg BUILDKIT_INLINE_CACHE=1 .
13
13
 
14
- docker push vitobotta/hetzner-k3s:v0.4.6
14
+ docker push vitobotta/hetzner-k3s:v0.5.0
@@ -1,11 +1,15 @@
1
1
  ---
2
- hetzner_token: blah
2
+ hetzner_token: <your token>
3
3
  cluster_name: test
4
- kubeconfig_path: "../kubeconfig"
4
+ kubeconfig_path: "./kubeconfig"
5
5
  k3s_version: v1.21.3+k3s1
6
- ssh_key_path: "~/.ssh/id_rsa.pub"
6
+ public_ssh_key_path: "~/.ssh/id_rsa.pub"
7
+ private_ssh_key_path: "~/.ssh/id_rsa"
8
+ ssh_allowed_networks:
9
+ - 0.0.0.0/0
7
10
  verify_host_key: false
8
11
  location: nbg1
12
+ schedule_workloads_on_masters: false
9
13
  masters:
10
14
  instance_type: cpx21
11
15
  instance_count: 3
@@ -14,5 +18,5 @@ worker_node_pools:
14
18
  instance_type: cpx21
15
19
  instance_count: 4
16
20
  - name: big
17
- instance_type: cp321
21
+ instance_type: cpx31
18
22
  instance_count: 2
data/hetzner-k3s.gemspec CHANGED
@@ -21,10 +21,10 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency "thor"
22
22
  spec.add_dependency "http"
23
23
  spec.add_dependency "net-ssh"
24
- spec.add_dependency "k8s-ruby"
25
24
  spec.add_dependency "sshkey"
26
25
  spec.add_dependency "ed25519"
27
26
  spec.add_dependency "bcrypt_pbkdf"
27
+ spec.add_dependency "subprocess"
28
28
 
29
29
  # Specify which files should be added to the gem when it is released.
30
30
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -36,11 +36,13 @@ module Hetzner
36
36
  end
37
37
 
38
38
  def make_request &block
39
- Timeout::timeout(5) do
39
+ retries ||= 0
40
+
41
+ Timeout::timeout(30) do
40
42
  block.call
41
43
  end
42
44
  rescue Timeout::Error
43
- retry
45
+ retry if (retries += 1) < 3
44
46
  end
45
47
  end
46
48
  end
@@ -5,7 +5,9 @@ module Hetzner
5
5
  @cluster_name = cluster_name
6
6
  end
7
7
 
8
- def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:)
8
+ def create(location:, instance_type:, instance_id:, firewall_id:, network_id:, ssh_key_id:, placement_group_id:, image:, additional_packages: [])
9
+ @additional_packages = additional_packages
10
+
9
11
  puts
10
12
 
11
13
  server_name = "#{cluster_name}-#{instance_type}-#{instance_id}"
@@ -21,7 +23,7 @@ module Hetzner
21
23
  server_config = {
22
24
  name: server_name,
23
25
  location: location,
24
- image: "ubuntu-20.04",
26
+ image: image,
25
27
  firewalls: [
26
28
  { firewall: firewall_id }
27
29
  ],
@@ -70,17 +72,20 @@ module Hetzner
70
72
 
71
73
  private
72
74
 
73
- attr_reader :hetzner_client, :cluster_name
75
+ attr_reader :hetzner_client, :cluster_name, :additional_packages
74
76
 
75
77
  def find_server(server_name)
76
- hetzner_client.get("/servers")["servers"].detect{ |network| network["name"] == server_name }
78
+ hetzner_client.get("/servers?sort=created:desc")["servers"].detect{ |network| network["name"] == server_name }
77
79
  end
78
80
 
79
81
  def user_data
82
+ packages = ["fail2ban"]
83
+ packages += additional_packages if additional_packages
84
+ packages = "'" + packages.join("', '") + "'"
85
+
80
86
  <<~EOS
81
87
  #cloud-config
82
- packages:
83
- - fail2ban
88
+ packages: [#{packages}]
84
89
  runcmd:
85
90
  - sed -i 's/[#]*PermitRootLogin yes/PermitRootLogin prohibit-password/g' /etc/ssh/sshd_config
86
91
  - sed -i 's/[#]*PasswordAuthentication yes/PasswordAuthentication no/g' /etc/ssh/sshd_config
@@ -90,7 +95,7 @@ module Hetzner
90
95
  - rm /etc/resolv.conf
91
96
  - echo "nameserver 1.1.1.1" > /etc/resolv.conf
92
97
  - echo "nameserver 1.0.0.1" >> /etc/resolv.conf
93
- EOS
98
+ EOS
94
99
  end
95
100
 
96
101
  end
@@ -3,10 +3,12 @@ require "http"
3
3
  require "sshkey"
4
4
  require 'ipaddr'
5
5
  require 'open-uri'
6
+ require "yaml"
6
7
 
7
8
  require_relative "cluster"
8
9
  require_relative "version"
9
10
 
11
+
10
12
  module Hetzner
11
13
  module K3s
12
14
  class CLI < Thor
@@ -24,7 +26,6 @@ module Hetzner
24
26
 
25
27
  def create_cluster
26
28
  validate_config_file :create
27
-
28
29
  Cluster.new(hetzner_client: hetzner_client, hetzner_token: find_hetzner_token).create configuration: configuration
29
30
  end
30
31
 
@@ -64,14 +65,17 @@ module Hetzner
64
65
  if File.exists?(config_file_path)
65
66
  begin
66
67
  @configuration = YAML.load_file(options[:config_file])
67
- raise "invalid" unless configuration.is_a? Hash
68
- rescue
68
+ unless configuration.is_a? Hash
69
+ raise "Configuration is invalid"
70
+ exit 1
71
+ end
72
+ rescue => e
69
73
  puts "Please ensure that the config file is a correct YAML manifest."
70
- return
74
+ exit 1
71
75
  end
72
76
  else
73
77
  puts "Please specify a correct path for the config file."
74
- return
78
+ exit 1
75
79
  end
76
80
 
77
81
  @errors = []
@@ -91,12 +95,12 @@ module Hetzner
91
95
  validate_masters
92
96
  validate_worker_node_pools
93
97
  validate_verify_host_key
98
+ validate_additional_packages
94
99
  when :delete
95
100
  validate_kubeconfig_path_must_exist
96
101
  when :upgrade
97
102
  validate_kubeconfig_path_must_exist
98
103
  validate_new_k3s_version
99
- validate_new_k3s_version_must_be_more_recent
100
104
  end
101
105
 
102
106
  errors.flatten!
@@ -198,7 +202,7 @@ module Hetzner
198
202
 
199
203
  def find_available_releases
200
204
  @available_releases ||= begin
201
- response = HTTP.get("https://api.github.com/repos/k3s-io/k3s/tags").body
205
+ response = HTTP.get("https://api.github.com/repos/k3s-io/k3s/tags?per_page=999").body
202
206
  JSON.parse(response).map { |hash| hash["name"] }
203
207
  end
204
208
  rescue
@@ -271,36 +275,6 @@ module Hetzner
271
275
  schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
272
276
  end
273
277
 
274
- def validate_new_k3s_version_must_be_more_recent
275
- return if options[:force] == "true"
276
- return unless kubernetes_client
277
-
278
- begin
279
- Timeout::timeout(5) do
280
- servers = kubernetes_client.api("v1").resource("nodes").list
281
-
282
- if servers.size == 0
283
- errors << "The cluster seems to have no nodes, nothing to upgrade"
284
- else
285
- available_releases = find_available_releases
286
-
287
- current_k3s_version = servers.first.dig(:status, :nodeInfo, :kubeletVersion)
288
- current_k3s_version_index = available_releases.index(current_k3s_version) || 1000
289
-
290
- new_k3s_version = options[:new_k3s_version]
291
- new_k3s_version_index = available_releases.index(new_k3s_version) || 1000
292
-
293
- unless new_k3s_version_index < current_k3s_version_index
294
- errors << "The new k3s version must be more recent than the current one"
295
- end
296
- end
297
- end
298
-
299
- rescue Timeout::Error
300
- puts "Cannot upgrade: Unable to fetch nodes from Kubernetes API. Is the cluster online?"
301
- end
302
- end
303
-
304
278
  def validate_instance_group(instance_group, workers: true)
305
279
  instance_group_errors = []
306
280
 
@@ -333,17 +307,6 @@ module Hetzner
333
307
  errors << instance_group_errors
334
308
  end
335
309
 
336
- def kubernetes_client
337
- return @kubernetes_client if @kubernetes_client
338
-
339
- config_hash = YAML.load_file(File.expand_path(configuration["kubeconfig_path"]))
340
- config_hash['current-context'] = configuration["cluster_name"]
341
- @kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
342
- rescue
343
- errors << "Cannot connect to the Kubernetes cluster"
344
- false
345
- end
346
-
347
310
  def validate_verify_host_key
348
311
  return unless [true, false].include?(configuration.fetch("public_ssh_key_path", false))
349
312
  errors << "Please set the verify_host_key option to either true or false"
@@ -396,6 +359,11 @@ module Hetzner
396
359
  end
397
360
  end
398
361
 
362
+ def validate_additional_packages
363
+ additional_packages = configuration.dig("additional_packages")
364
+ errors << "Invalid additional packages configuration - it should be an array" if additional_packages && !additional_packages.is_a?(Array)
365
+ end
366
+
399
367
  end
400
368
  end
401
369
  end
@@ -2,8 +2,8 @@ require 'thread'
2
2
  require 'net/ssh'
3
3
  require "securerandom"
4
4
  require "base64"
5
- require "k8s-ruby"
6
5
  require 'timeout'
6
+ require "subprocess"
7
7
 
8
8
  require_relative "../infra/client"
9
9
  require_relative "../infra/firewall"
@@ -13,10 +13,12 @@ require_relative "../infra/server"
13
13
  require_relative "../infra/load_balancer"
14
14
  require_relative "../infra/placement_group"
15
15
 
16
- require_relative "../k3s/client_patch"
16
+ require_relative "../utils"
17
17
 
18
18
 
19
19
  class Cluster
20
+ include Utils
21
+
20
22
  def initialize(hetzner_client:, hetzner_token:)
21
23
  @hetzner_client = hetzner_client
22
24
  @hetzner_token = hetzner_token
@@ -36,6 +38,7 @@ class Cluster
36
38
  @verify_host_key = configuration.fetch("verify_host_key", false)
37
39
  @servers = []
38
40
  @networks = configuration.dig("ssh_allowed_networks")
41
+ @enable_ipsec_encryption = configuration.fetch("enable_ipsec_encryption", false)
39
42
 
40
43
  create_resources
41
44
 
@@ -76,9 +79,10 @@ class Cluster
76
79
 
77
80
  attr_reader :hetzner_client, :cluster_name, :kubeconfig_path, :k3s_version,
78
81
  :masters_config, :worker_node_pools,
79
- :location, :public_ssh_key_path, :kubernetes_client,
82
+ :location, :public_ssh_key_path,
80
83
  :hetzner_token, :tls_sans, :new_k3s_version, :configuration,
81
- :config_file, :verify_host_key, :networks, :private_ssh_key_path, :configuration
84
+ :config_file, :verify_host_key, :networks, :private_ssh_key_path,
85
+ :configuration, :enable_ipsec_encryption
82
86
 
83
87
 
84
88
  def latest_k3s_version
@@ -120,7 +124,9 @@ class Cluster
120
124
  firewall_id: firewall_id,
121
125
  network_id: network_id,
122
126
  ssh_key_id: ssh_key_id,
123
- placement_group_id: placement_group_id
127
+ placement_group_id: placement_group_id,
128
+ image: image,
129
+ additional_packages: additional_packages,
124
130
  }
125
131
  end
126
132
 
@@ -144,22 +150,23 @@ class Cluster
144
150
  firewall_id: firewall_id,
145
151
  network_id: network_id,
146
152
  ssh_key_id: ssh_key_id,
147
- placement_group_id: placement_group_id
153
+ placement_group_id: placement_group_id,
154
+ image: image,
155
+ additional_packages: additional_packages,
148
156
  }
149
157
  end
150
158
  end
151
159
 
152
160
  threads = server_configs.map do |server_config|
153
161
  Thread.new do
154
- servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(server_config)
162
+ servers << Hetzner::Server.new(hetzner_client: hetzner_client, cluster_name: cluster_name).create(**server_config)
155
163
  end
156
164
  end
157
165
 
158
166
  threads.each(&:join) unless threads.empty?
159
167
 
160
- if server_configs.size != servers.size
161
- puts "Something went wrong while creating some servers, please try again."
162
- exit 1
168
+ while servers.size != server_configs.size
169
+ sleep 1
163
170
  end
164
171
 
165
172
  puts
@@ -206,31 +213,78 @@ class Cluster
206
213
  end
207
214
 
208
215
  def upgrade_cluster
209
- resources = K8s::Resource.from_files(ugrade_plan_manifest_path)
210
-
211
- begin
212
- kubernetes_client.api("upgrade.cattle.io/v1").resource("plans").get("k3s-server", namespace: "system-upgrade")
213
-
214
- puts "Aborting - an upgrade is already in progress."
215
-
216
- rescue K8s::Error::NotFound
217
- resources.each do |resource|
218
- kubernetes_client.create_resource(resource)
219
- end
220
-
221
- puts "Upgrade will now start. Run `watch kubectl get nodes` to see the nodes being upgraded. This should take a few minutes for a small cluster."
222
- puts "The API server may be briefly unavailable during the upgrade of the controlplane."
223
-
224
- configuration["k3s_version"] = new_k3s_version
216
+ worker_upgrade_concurrency = workers.size - 1
217
+ worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
225
218
 
226
- File.write(config_file, configuration.to_yaml)
227
- end
219
+ cmd = <<~EOS
220
+ kubectl apply -f - <<-EOF
221
+ apiVersion: upgrade.cattle.io/v1
222
+ kind: Plan
223
+ metadata:
224
+ name: k3s-server
225
+ namespace: system-upgrade
226
+ labels:
227
+ k3s-upgrade: server
228
+ spec:
229
+ concurrency: 1
230
+ version: #{new_k3s_version}
231
+ nodeSelector:
232
+ matchExpressions:
233
+ - {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
234
+ serviceAccountName: system-upgrade
235
+ tolerations:
236
+ - key: "CriticalAddonsOnly"
237
+ operator: "Equal"
238
+ value: "true"
239
+ effect: "NoExecute"
240
+ cordon: true
241
+ upgrade:
242
+ image: rancher/k3s-upgrade
243
+ EOF
244
+ EOS
245
+
246
+ run cmd, kubeconfig_path: kubeconfig_path
247
+
248
+ cmd = <<~EOS
249
+ kubectl apply -f - <<-EOF
250
+ apiVersion: upgrade.cattle.io/v1
251
+ kind: Plan
252
+ metadata:
253
+ name: k3s-agent
254
+ namespace: system-upgrade
255
+ labels:
256
+ k3s-upgrade: agent
257
+ spec:
258
+ concurrency: #{worker_upgrade_concurrency}
259
+ version: #{new_k3s_version}
260
+ nodeSelector:
261
+ matchExpressions:
262
+ - {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
263
+ serviceAccountName: system-upgrade
264
+ prepare:
265
+ image: rancher/k3s-upgrade
266
+ args: ["prepare", "k3s-server"]
267
+ cordon: true
268
+ upgrade:
269
+ image: rancher/k3s-upgrade
270
+ EOF
271
+ EOS
272
+
273
+ run cmd, kubeconfig_path: kubeconfig_path
274
+
275
+ puts "Upgrade will now start. Run `watch kubectl get nodes` to see the nodes being upgraded. This should take a few minutes for a small cluster."
276
+ puts "The API server may be briefly unavailable during the upgrade of the controlplane."
277
+
278
+ configuration["k3s_version"] = new_k3s_version
279
+
280
+ File.write(config_file, configuration.to_yaml)
228
281
  end
229
282
 
230
283
 
231
284
  def master_script(master)
232
285
  server = master == first_master ? " --cluster-init " : " --server https://#{first_master_private_ip}:6443 "
233
286
  flannel_interface = find_flannel_interface(master)
287
+ flannel_ipsec = enable_ipsec_encryption ? " --flannel-backend=ipsec " : " "
234
288
 
235
289
  taint = schedule_workloads_on_masters? ? " " : " --node-taint CriticalAddonsOnly=true:NoExecute "
236
290
 
@@ -245,6 +299,7 @@ class Cluster
245
299
  --node-name="$(hostname -f)" \
246
300
  --cluster-cidr=10.244.0.0/16 \
247
301
  --etcd-expose-metrics=true \
302
+ #{flannel_ipsec} \
248
303
  --kube-controller-manager-arg="address=0.0.0.0" \
249
304
  --kube-controller-manager-arg="bind-address=0.0.0.0" \
250
305
  --kube-proxy-arg="metrics-bind-address=0.0.0.0" \
@@ -316,201 +371,71 @@ class Cluster
316
371
  end
317
372
 
318
373
  def deploy_cloud_controller_manager
374
+ check_kubectl
375
+
319
376
  puts
320
377
  puts "Deploying Hetzner Cloud Controller Manager..."
321
378
 
322
- begin
323
- kubernetes_client.api("v1").resource("secrets").get("hcloud", namespace: "kube-system")
324
-
325
- rescue K8s::Error::NotFound
326
- secret = K8s::Resource.new(
327
- apiVersion: "v1",
328
- kind: "Secret",
329
- metadata: {
330
- namespace: 'kube-system',
331
- name: 'hcloud',
332
- },
333
- data: {
334
- network: Base64.encode64(cluster_name),
335
- token: Base64.encode64(hetzner_token)
336
- }
337
- )
338
-
339
- kubernetes_client.api('v1').resource('secrets').create_resource(secret)
340
- end
341
-
342
-
343
- manifest = fetch_manifest("https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml")
379
+ cmd = <<~EOS
380
+ kubectl apply -f - <<-EOF
381
+ apiVersion: "v1"
382
+ kind: "Secret"
383
+ metadata:
384
+ namespace: 'kube-system'
385
+ name: 'hcloud'
386
+ stringData:
387
+ network: "#{cluster_name}"
388
+ token: "#{hetzner_token}"
389
+ EOF
390
+ EOS
344
391
 
345
- File.write("/tmp/cloud-controller-manager.yaml", manifest)
392
+ run cmd, kubeconfig_path: kubeconfig_path
346
393
 
347
- resources = K8s::Resource.from_files("/tmp/cloud-controller-manager.yaml")
394
+ cmd = "kubectl apply -f https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/latest/download/ccm-networks.yaml"
348
395
 
349
- begin
350
- kubernetes_client.api("apps/v1").resource("deployments").get("hcloud-cloud-controller-manager", namespace: "kube-system")
351
-
352
- resources.each do |resource|
353
- kubernetes_client.update_resource(resource)
354
- end
355
-
356
- rescue K8s::Error::NotFound
357
- resources.each do |resource|
358
- kubernetes_client.create_resource(resource)
359
- end
360
-
361
- end
396
+ run cmd, kubeconfig_path: kubeconfig_path
362
397
 
363
398
  puts "...Cloud Controller Manager deployed"
364
- rescue Excon::Error::Socket
365
- retry
366
- end
367
-
368
- def fetch_manifest(url)
369
- retries ||= 1
370
- HTTP.follow.get(url).body
371
- rescue
372
- retry if (retries += 1) <= 10
373
399
  end
374
400
 
375
401
  def deploy_system_upgrade_controller
402
+ check_kubectl
403
+
376
404
  puts
377
405
  puts "Deploying k3s System Upgrade Controller..."
378
406
 
379
- manifest = HTTP.follow.get("https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.0/system-upgrade-controller.yaml").body
380
-
381
- File.write("/tmp/system-upgrade-controller.yaml", manifest)
382
-
383
- resources = K8s::Resource.from_files("/tmp/system-upgrade-controller.yaml")
407
+ cmd = "kubectl apply -f https://github.com/rancher/system-upgrade-controller/releases/download/v0.8.1/system-upgrade-controller.yaml"
384
408
 
385
- begin
386
- kubernetes_client.api("apps/v1").resource("deployments").get("system-upgrade-controller", namespace: "system-upgrade")
387
-
388
- resources.each do |resource|
389
- kubernetes_client.update_resource(resource)
390
- end
391
-
392
- rescue K8s::Error::NotFound
393
- resources.each do |resource|
394
- kubernetes_client.create_resource(resource)
395
- end
396
-
397
- end
409
+ run cmd, kubeconfig_path: kubeconfig_path
398
410
 
399
411
  puts "...k3s System Upgrade Controller deployed"
400
- rescue Excon::Error::Socket
401
- retry
402
412
  end
403
413
 
404
414
  def deploy_csi_driver
415
+ check_kubectl
416
+
405
417
  puts
406
418
  puts "Deploying Hetzner CSI Driver..."
407
419
 
408
- begin
409
- kubernetes_client.api("v1").resource("secrets").get("hcloud-csi", namespace: "kube-system")
410
-
411
- rescue K8s::Error::NotFound
412
- secret = K8s::Resource.new(
413
- apiVersion: "v1",
414
- kind: "Secret",
415
- metadata: {
416
- namespace: 'kube-system',
417
- name: 'hcloud-csi',
418
- },
419
- data: {
420
- token: Base64.encode64(hetzner_token)
421
- }
422
- )
423
-
424
- kubernetes_client.api('v1').resource('secrets').create_resource(secret)
425
- end
426
-
427
-
428
- manifest = HTTP.follow.get("https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml").body
429
-
430
- File.write("/tmp/csi-driver.yaml", manifest)
420
+ cmd = <<~EOS
421
+ kubectl apply -f - <<-EOF
422
+ apiVersion: "v1"
423
+ kind: "Secret"
424
+ metadata:
425
+ namespace: 'kube-system'
426
+ name: 'hcloud-csi'
427
+ stringData:
428
+ token: "#{hetzner_token}"
429
+ EOF
430
+ EOS
431
431
 
432
- resources = K8s::Resource.from_files("/tmp/csi-driver.yaml")
432
+ run cmd, kubeconfig_path: kubeconfig_path
433
433
 
434
- begin
435
- kubernetes_client.api("apps/v1").resource("daemonsets").get("hcloud-csi-node", namespace: "kube-system")
434
+ cmd = "kubectl apply -f https://raw.githubusercontent.com/hetznercloud/csi-driver/v1.6.0/deploy/kubernetes/hcloud-csi.yml"
436
435
 
437
-
438
- resources.each do |resource|
439
- begin
440
- kubernetes_client.update_resource(resource)
441
- rescue K8s::Error::Invalid => e
442
- raise e unless e.message =~ /must be specified/i
443
- end
444
- end
445
-
446
- rescue K8s::Error::NotFound
447
- resources.each do |resource|
448
- kubernetes_client.create_resource(resource)
449
- end
450
-
451
- end
436
+ run cmd, kubeconfig_path: kubeconfig_path
452
437
 
453
438
  puts "...CSI Driver deployed"
454
- rescue Excon::Error::Socket
455
- retry
456
- end
457
-
458
- def wait_for_ssh(server)
459
- Timeout::timeout(5) do
460
- server_name = server["name"]
461
-
462
- puts "Waiting for server #{server_name} to be up..."
463
-
464
- loop do
465
- result = ssh(server, "echo UP")
466
- break if result == "UP"
467
- end
468
-
469
- puts "...server #{server_name} is now up."
470
- end
471
- rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error, IOError
472
- retry
473
- end
474
-
475
- def ssh(server, command, print_output: false)
476
- public_ip = server.dig("public_net", "ipv4", "ip")
477
- output = ""
478
-
479
- params = { verify_host_key: (verify_host_key ? :always : :never) }
480
-
481
- if private_ssh_key_path
482
- params[:keys] = [private_ssh_key_path]
483
- end
484
-
485
- Net::SSH.start(public_ip, "root", params) do |session|
486
- session.exec!(command) do |channel, stream, data|
487
- output << data
488
- puts data if print_output
489
- end
490
- end
491
- output.chop
492
- rescue Net::SSH::Disconnect => e
493
- retry unless e.message =~ /Too many authentication failures/
494
- rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
495
- retry
496
- rescue Net::SSH::AuthenticationFailed
497
- puts
498
- puts "Cannot continue: SSH authentication failed. Please ensure that the private SSH key is correct."
499
- exit 1
500
- rescue Net::SSH::HostKeyMismatch
501
- puts
502
- puts "Cannot continue: Unable to SSH into server with IP #{public_ip} because the existing fingerprint in the known_hosts file does not match that of the actual host key."
503
- puts "This is due to a security check but can also happen when creating a new server that gets assigned the same IP address as another server you've owned in the past."
504
- puts "If are sure no security is being violated here and you're just creating new servers, you can eiher remove the relevant lines from your known_hosts (see IPs from the cloud console) or disable host key verification by setting the option 'verify_host_key' to false in the configuration file for the cluster."
505
- exit 1
506
- end
507
-
508
- def kubernetes_client
509
- return @kubernetes_client if @kubernetes_client
510
-
511
- config_hash = YAML.load_file(kubeconfig_path)
512
- config_hash['current-context'] = cluster_name
513
- @kubernetes_client = K8s::Client.config(K8s::Config.new(config_hash))
514
439
  end
515
440
 
516
441
  def find_flannel_interface(server)
@@ -522,7 +447,7 @@ class Cluster
522
447
  end
523
448
 
524
449
  def all_servers
525
- @all_servers ||= hetzner_client.get("/servers")["servers"].select{ |server| belongs_to_cluster?(server) == true }
450
+ @all_servers ||= hetzner_client.get("/servers?sort=created:desc")["servers"].select{ |server| belongs_to_cluster?(server) == true }
526
451
  end
527
452
 
528
453
  def masters
@@ -590,63 +515,6 @@ class Cluster
590
515
  FileUtils.chmod "go-r", kubeconfig_path
591
516
  end
592
517
 
593
- def ugrade_plan_manifest_path
594
- worker_upgrade_concurrency = workers.size - 1
595
- worker_upgrade_concurrency = 1 if worker_upgrade_concurrency == 0
596
-
597
- manifest = <<~EOF
598
- apiVersion: upgrade.cattle.io/v1
599
- kind: Plan
600
- metadata:
601
- name: k3s-server
602
- namespace: system-upgrade
603
- labels:
604
- k3s-upgrade: server
605
- spec:
606
- concurrency: 1
607
- version: #{new_k3s_version}
608
- nodeSelector:
609
- matchExpressions:
610
- - {key: node-role.kubernetes.io/master, operator: In, values: ["true"]}
611
- serviceAccountName: system-upgrade
612
- tolerations:
613
- - key: "CriticalAddonsOnly"
614
- operator: "Equal"
615
- value: "true"
616
- effect: "NoExecute"
617
- cordon: true
618
- upgrade:
619
- image: rancher/k3s-upgrade
620
- ---
621
- apiVersion: upgrade.cattle.io/v1
622
- kind: Plan
623
- metadata:
624
- name: k3s-agent
625
- namespace: system-upgrade
626
- labels:
627
- k3s-upgrade: agent
628
- spec:
629
- concurrency: #{worker_upgrade_concurrency}
630
- version: #{new_k3s_version}
631
- nodeSelector:
632
- matchExpressions:
633
- - {key: node-role.kubernetes.io/master, operator: NotIn, values: ["true"]}
634
- serviceAccountName: system-upgrade
635
- prepare:
636
- image: rancher/k3s-upgrade
637
- args: ["prepare", "k3s-server"]
638
- cordon: true
639
- upgrade:
640
- image: rancher/k3s-upgrade
641
- EOF
642
-
643
- temp_file_path = "/tmp/k3s-upgrade-plan.yaml"
644
-
645
- File.write(temp_file_path, manifest)
646
-
647
- temp_file_path
648
- end
649
-
650
518
  def belongs_to_cluster?(server)
651
519
  server.dig("labels", "cluster") == cluster_name
652
520
  end
@@ -656,4 +524,19 @@ class Cluster
656
524
  schedule_workloads_on_masters ? !!schedule_workloads_on_masters : false
657
525
  end
658
526
 
527
+ def image
528
+ configuration.dig("image") || "ubuntu-20.04"
529
+ end
530
+
531
+ def additional_packages
532
+ configuration.dig("additional_packages") || []
533
+ end
534
+
535
+ def check_kubectl
536
+ unless which("kubectl")
537
+ puts "Please ensure kubectl is installed and in your PATH."
538
+ exit 1
539
+ end
540
+ end
541
+
659
542
  end
@@ -1,5 +1,5 @@
1
1
  module Hetzner
2
2
  module K3s
3
- VERSION = "0.4.6"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
@@ -0,0 +1,99 @@
1
+ module Utils
2
+ def which(cmd)
3
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
4
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
5
+ exts.each do |ext|
6
+ exe = File.join(path, "#{cmd}#{ext}")
7
+ return exe if File.executable?(exe) && !File.directory?(exe)
8
+ end
9
+ end
10
+ nil
11
+ end
12
+
13
+ def run(command, kubeconfig_path:)
14
+ env = ENV.to_hash.merge({
15
+ "KUBECONFIG" => kubeconfig_path
16
+ })
17
+
18
+ cmd_path = "/tmp/cli.cmd"
19
+
20
+ File.open(cmd_path, "w") do |f|
21
+ f.write("set -euo pipefail\n")
22
+ f.write(command)
23
+ end
24
+
25
+ FileUtils.chmod("+x", cmd_path)
26
+
27
+ begin
28
+ process = nil
29
+
30
+ at_exit do
31
+ begin
32
+ process&.send_signal("SIGTERM")
33
+ rescue Errno::ESRCH, Interrupt
34
+ end
35
+ end
36
+
37
+ Subprocess.check_call(["bash", "-c", cmd_path], env: env) do |p|
38
+ process = p
39
+ end
40
+
41
+ rescue Subprocess::NonZeroExit
42
+ puts "Command failed: non-zero exit code"
43
+ exit 1
44
+ rescue Interrupt
45
+ puts "Command interrupted"
46
+ exit 1
47
+ end
48
+ end
49
+
50
+ def wait_for_ssh(server)
51
+ Timeout::timeout(5) do
52
+ server_name = server["name"]
53
+
54
+ puts "Waiting for server #{server_name} to be up..."
55
+
56
+ loop do
57
+ result = ssh(server, "echo UP")
58
+ break if result == "UP"
59
+ end
60
+
61
+ puts "...server #{server_name} is now up."
62
+ end
63
+ rescue Errno::ENETUNREACH, Errno::EHOSTUNREACH, Timeout::Error, IOError
64
+ retry
65
+ end
66
+
67
+ def ssh(server, command, print_output: false)
68
+ public_ip = server.dig("public_net", "ipv4", "ip")
69
+ output = ""
70
+
71
+ params = { verify_host_key: (verify_host_key ? :always : :never) }
72
+
73
+ if private_ssh_key_path
74
+ params[:keys] = [private_ssh_key_path]
75
+ end
76
+
77
+ Net::SSH.start(public_ip, "root", params) do |session|
78
+ session.exec!(command) do |channel, stream, data|
79
+ output << data
80
+ puts data if print_output
81
+ end
82
+ end
83
+ output.chop
84
+ rescue Net::SSH::Disconnect => e
85
+ retry unless e.message =~ /Too many authentication failures/
86
+ rescue Net::SSH::ConnectionTimeout, Errno::ECONNREFUSED, Errno::ENETUNREACH, Errno::EHOSTUNREACH
87
+ retry
88
+ rescue Net::SSH::AuthenticationFailed
89
+ puts
90
+ puts "Cannot continue: SSH authentication failed. Please ensure that the private SSH key is correct."
91
+ exit 1
92
+ rescue Net::SSH::HostKeyMismatch
93
+ puts
94
+ puts "Cannot continue: Unable to SSH into server with IP #{public_ip} because the existing fingerprint in the known_hosts file does not match that of the actual host key."
95
+ puts "This is due to a security check but can also happen when creating a new server that gets assigned the same IP address as another server you've owned in the past."
96
+ puts "If are sure no security is being violated here and you're just creating new servers, you can eiher remove the relevant lines from your known_hosts (see IPs from the cloud console) or disable host key verification by setting the option 'verify_host_key' to false in the configuration file for the cluster."
97
+ exit 1
98
+ end
99
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hetzner-k3s
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.6
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vito Botta
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-13 00:00:00.000000000 Z
11
+ date: 2022-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: k8s-ruby
56
+ name: sshkey
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: sshkey
70
+ name: ed25519
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: ed25519
84
+ name: bcrypt_pbkdf
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -95,7 +95,7 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: bcrypt_pbkdf
98
+ name: subprocess
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -119,6 +119,7 @@ extra_rdoc_files: []
119
119
  files:
120
120
  - ".gitignore"
121
121
  - ".rspec"
122
+ - ".ruby-version"
122
123
  - ".travis.yml"
123
124
  - CODE_OF_CONDUCT.md
124
125
  - Dockerfile
@@ -144,9 +145,9 @@ files:
144
145
  - lib/hetzner/infra/server.rb
145
146
  - lib/hetzner/infra/ssh_key.rb
146
147
  - lib/hetzner/k3s/cli.rb
147
- - lib/hetzner/k3s/client_patch.rb
148
148
  - lib/hetzner/k3s/cluster.rb
149
149
  - lib/hetzner/k3s/version.rb
150
+ - lib/hetzner/utils.rb
150
151
  homepage: https://github.com/vitobotta/hetzner-k3s
151
152
  licenses:
152
153
  - MIT
@@ -154,7 +155,7 @@ metadata:
154
155
  homepage_uri: https://github.com/vitobotta/hetzner-k3s
155
156
  source_code_uri: https://github.com/vitobotta/hetzner-k3s
156
157
  changelog_uri: https://github.com/vitobotta/hetzner-k3s
157
- post_install_message:
158
+ post_install_message:
158
159
  rdoc_options: []
159
160
  require_paths:
160
161
  - lib
@@ -169,8 +170,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
170
  - !ruby/object:Gem::Version
170
171
  version: '0'
171
172
  requirements: []
172
- rubygems_version: 3.1.4
173
- signing_key:
173
+ rubygems_version: 3.3.3
174
+ signing_key:
174
175
  specification_version: 4
175
176
  summary: A CLI to create a Kubernetes cluster in Hetzner Cloud very quickly using
176
177
  k3s.
@@ -1,38 +0,0 @@
1
- module K8s
2
- class ResourceClient
3
- def initialize(transport, api_client, api_resource, namespace: nil, resource_class: K8s::Resource)
4
- @transport = transport
5
- @api_client = api_client
6
- @api_resource = api_resource
7
- @namespace = namespace
8
- @resource_class = resource_class
9
-
10
- if @api_resource.name.include? '/'
11
- @resource, @subresource = @api_resource.name.split('/', 2)
12
- else
13
- @resource = @api_resource.name
14
- @subresource = nil
15
- end
16
-
17
- # fail "Resource #{api_resource.name} is not namespaced" unless api_resource.namespaced || !namespace
18
- end
19
-
20
- def path(name = nil, subresource: @subresource, namespace: @namespace)
21
- namespace_part = namespace ? ['namespaces', namespace] : []
22
-
23
- if namespaced?
24
- if name && subresource
25
- @api_client.path(*namespace_part, @resource, name, subresource)
26
- elsif name
27
- @api_client.path(*namespace_part, @resource, name)
28
- else namespaced?
29
- @api_client.path(*namespace_part, @resource)
30
- end
31
- elsif name
32
- @api_client.path(@resource, name)
33
- else
34
- @api_client.path(@resource)
35
- end
36
- end
37
- end
38
- end