kubeclient 0.3.0 → 4.9.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of kubeclient might be problematic. Click here for more details.

Files changed (63) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/actions.yml +35 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +29 -0
  5. data/CHANGELOG.md +208 -0
  6. data/Gemfile +3 -0
  7. data/README.md +706 -57
  8. data/RELEASING.md +69 -0
  9. data/Rakefile +3 -5
  10. data/kubeclient.gemspec +19 -11
  11. data/lib/kubeclient/aws_eks_credentials.rb +46 -0
  12. data/lib/kubeclient/common.rb +597 -161
  13. data/lib/kubeclient/config.rb +195 -0
  14. data/lib/kubeclient/entity_list.rb +7 -2
  15. data/lib/kubeclient/exec_credentials.rb +89 -0
  16. data/lib/kubeclient/gcp_auth_provider.rb +19 -0
  17. data/lib/kubeclient/gcp_command_credentials.rb +31 -0
  18. data/lib/kubeclient/google_application_default_credentials.rb +31 -0
  19. data/lib/kubeclient/http_error.rb +25 -0
  20. data/lib/kubeclient/missing_kind_compatibility.rb +68 -0
  21. data/lib/kubeclient/oidc_auth_provider.rb +52 -0
  22. data/lib/kubeclient/resource.rb +11 -0
  23. data/lib/kubeclient/resource_not_found_error.rb +4 -0
  24. data/lib/kubeclient/version.rb +1 -1
  25. data/lib/kubeclient/watch_stream.rb +71 -28
  26. data/lib/kubeclient.rb +25 -82
  27. metadata +140 -114
  28. data/.travis.yml +0 -6
  29. data/lib/kubeclient/kube_exception.rb +0 -13
  30. data/lib/kubeclient/watch_notice.rb +0 -7
  31. data/test/json/created_namespace_b3.json +0 -20
  32. data/test/json/created_secret.json +0 -16
  33. data/test/json/created_service_b3.json +0 -31
  34. data/test/json/empty_pod_list_b3.json +0 -9
  35. data/test/json/endpoint_list_b3.json +0 -48
  36. data/test/json/entity_list_b3.json +0 -56
  37. data/test/json/event_list_b3.json +0 -35
  38. data/test/json/namespace_b3.json +0 -13
  39. data/test/json/namespace_exception_b3.json +0 -8
  40. data/test/json/namespace_list_b3.json +0 -32
  41. data/test/json/node_b3.json +0 -29
  42. data/test/json/node_list_b3.json +0 -37
  43. data/test/json/pod_b3.json +0 -92
  44. data/test/json/pod_list_b3.json +0 -75
  45. data/test/json/replication_controller_b3.json +0 -57
  46. data/test/json/replication_controller_list_b3.json +0 -64
  47. data/test/json/secret_list_b3.json +0 -44
  48. data/test/json/service_b3.json +0 -33
  49. data/test/json/service_illegal_json_404.json +0 -1
  50. data/test/json/service_list_b3.json +0 -97
  51. data/test/json/service_update_b3.json +0 -22
  52. data/test/json/versions_list.json +0 -6
  53. data/test/json/watch_stream_b3.json +0 -3
  54. data/test/test_helper.rb +0 -4
  55. data/test/test_kubeclient.rb +0 -407
  56. data/test/test_namespace.rb +0 -53
  57. data/test/test_node.rb +0 -25
  58. data/test/test_pod.rb +0 -21
  59. data/test/test_replication_controller.rb +0 -24
  60. data/test/test_secret.rb +0 -58
  61. data/test/test_service.rb +0 -136
  62. data/test/test_watch.rb +0 -37
  63. data/test/valid_token_file +0 -1
@@ -0,0 +1,195 @@
1
+ require 'yaml'
2
+ require 'base64'
3
+ require 'pathname'
4
+
5
+ module Kubeclient
6
+ # Kubernetes client configuration class
7
+ class Config
8
+ # Kubernetes client configuration context class
9
+ class Context
10
+ attr_reader :api_endpoint, :api_version, :ssl_options, :auth_options, :namespace
11
+
12
+ def initialize(api_endpoint, api_version, ssl_options, auth_options, namespace)
13
+ @api_endpoint = api_endpoint
14
+ @api_version = api_version
15
+ @ssl_options = ssl_options
16
+ @auth_options = auth_options
17
+ @namespace = namespace
18
+ end
19
+ end
20
+
21
+ # data (Hash) - Parsed kubeconfig data.
22
+ # kcfg_path (string) - Base directory for resolving relative references to external files.
23
+ # If set to nil, all external lookups & commands are disabled (even for absolute paths).
24
+ # See also the more convenient Config.read
25
+ def initialize(data, kcfg_path)
26
+ @kcfg = data
27
+ @kcfg_path = kcfg_path
28
+ raise 'Unknown kubeconfig version' if @kcfg['apiVersion'] != 'v1'
29
+ end
30
+
31
+ # Builds Config instance by parsing given file, with lookups relative to file's directory.
32
+ def self.read(filename)
33
+ parsed =
34
+ if RUBY_VERSION >= '2.6'
35
+ YAML.safe_load(File.read(filename), permitted_classes: [Date, Time])
36
+ else
37
+ YAML.safe_load(File.read(filename), [Date, Time])
38
+ end
39
+ Config.new(parsed, File.dirname(filename))
40
+ end
41
+
42
+ def contexts
43
+ @kcfg['contexts'].map { |x| x['name'] }
44
+ end
45
+
46
+ def context(context_name = nil)
47
+ cluster, user, namespace = fetch_context(context_name || @kcfg['current-context'])
48
+
49
+ if user.key?('exec')
50
+ exec_opts = expand_command_option(user['exec'], 'command')
51
+ user['exec_result'] = ExecCredentials.run(exec_opts)
52
+ end
53
+
54
+ ca_cert_data = fetch_cluster_ca_data(cluster)
55
+ client_cert_data = fetch_user_cert_data(user)
56
+ client_key_data = fetch_user_key_data(user)
57
+ auth_options = fetch_user_auth_options(user)
58
+
59
+ ssl_options = {}
60
+
61
+ if !ca_cert_data.nil?
62
+ cert_store = OpenSSL::X509::Store.new
63
+ cert_store.add_cert(OpenSSL::X509::Certificate.new(ca_cert_data))
64
+ ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_PEER
65
+ ssl_options[:cert_store] = cert_store
66
+ else
67
+ ssl_options[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE
68
+ end
69
+
70
+ unless client_cert_data.nil?
71
+ ssl_options[:client_cert] = OpenSSL::X509::Certificate.new(client_cert_data)
72
+ end
73
+
74
+ unless client_key_data.nil?
75
+ ssl_options[:client_key] = OpenSSL::PKey.read(client_key_data)
76
+ end
77
+
78
+ Context.new(cluster['server'], @kcfg['apiVersion'], ssl_options, auth_options, namespace)
79
+ end
80
+
81
+ private
82
+
83
+ def allow_external_lookups?
84
+ @kcfg_path != nil
85
+ end
86
+
87
+ def ext_file_path(path)
88
+ unless allow_external_lookups?
89
+ raise "Kubeclient::Config: external lookups disabled, can't load '#{path}'"
90
+ end
91
+ Pathname(path).absolute? ? path : File.join(@kcfg_path, path)
92
+ end
93
+
94
+ def ext_command_path(path)
95
+ unless allow_external_lookups?
96
+ raise "Kubeclient::Config: external lookups disabled, can't execute '#{path}'"
97
+ end
98
+ # Like go client https://github.com/kubernetes/kubernetes/pull/59495#discussion_r171138995,
99
+ # distinguish 3 cases:
100
+ # - absolute (e.g. /path/to/foo)
101
+ # - $PATH-based (e.g. curl)
102
+ # - relative to config file's dir (e.g. ./foo)
103
+ if Pathname(path).absolute?
104
+ path
105
+ elsif File.basename(path) == path
106
+ path
107
+ else
108
+ File.join(@kcfg_path, path)
109
+ end
110
+ end
111
+
112
+ def fetch_context(context_name)
113
+ context = @kcfg['contexts'].detect do |x|
114
+ break x['context'] if x['name'] == context_name
115
+ end
116
+
117
+ raise KeyError, "Unknown context #{context_name}" unless context
118
+
119
+ cluster = @kcfg['clusters'].detect do |x|
120
+ break x['cluster'] if x['name'] == context['cluster']
121
+ end
122
+
123
+ raise KeyError, "Unknown cluster #{context['cluster']}" unless cluster
124
+
125
+ user = @kcfg['users'].detect do |x|
126
+ break x['user'] if x['name'] == context['user']
127
+ end || {}
128
+
129
+ namespace = context['namespace']
130
+
131
+ [cluster, user, namespace]
132
+ end
133
+
134
+ def fetch_cluster_ca_data(cluster)
135
+ if cluster.key?('certificate-authority')
136
+ File.read(ext_file_path(cluster['certificate-authority']))
137
+ elsif cluster.key?('certificate-authority-data')
138
+ Base64.decode64(cluster['certificate-authority-data'])
139
+ end
140
+ end
141
+
142
+ def fetch_user_cert_data(user)
143
+ if user.key?('client-certificate')
144
+ File.read(ext_file_path(user['client-certificate']))
145
+ elsif user.key?('client-certificate-data')
146
+ Base64.decode64(user['client-certificate-data'])
147
+ elsif user.key?('exec_result') && user['exec_result'].key?('clientCertificateData')
148
+ user['exec_result']['clientCertificateData']
149
+ end
150
+ end
151
+
152
+ def fetch_user_key_data(user)
153
+ if user.key?('client-key')
154
+ File.read(ext_file_path(user['client-key']))
155
+ elsif user.key?('client-key-data')
156
+ Base64.decode64(user['client-key-data'])
157
+ elsif user.key?('exec_result') && user['exec_result'].key?('clientKeyData')
158
+ user['exec_result']['clientKeyData']
159
+ end
160
+ end
161
+
162
+ def fetch_user_auth_options(user)
163
+ options = {}
164
+ if user.key?('token')
165
+ options[:bearer_token] = user['token']
166
+ elsif user.key?('exec_result') && user['exec_result'].key?('token')
167
+ options[:bearer_token] = user['exec_result']['token']
168
+ elsif user.key?('auth-provider')
169
+ options[:bearer_token] = fetch_token_from_provider(user['auth-provider'])
170
+ else
171
+ %w[username password].each do |attr|
172
+ options[attr.to_sym] = user[attr] if user.key?(attr)
173
+ end
174
+ end
175
+ options
176
+ end
177
+
178
+ def fetch_token_from_provider(auth_provider)
179
+ case auth_provider['name']
180
+ when 'gcp'
181
+ config = expand_command_option(auth_provider['config'], 'cmd-path')
182
+ Kubeclient::GCPAuthProvider.token(config)
183
+ when 'oidc'
184
+ Kubeclient::OIDCAuthProvider.token(auth_provider['config'])
185
+ end
186
+ end
187
+
188
+ def expand_command_option(config, key)
189
+ config = config.dup
190
+ config[key] = ext_command_path(config[key]) if config[key]
191
+
192
+ config
193
+ end
194
+ end
195
+ end
@@ -3,14 +3,19 @@ module Kubeclient
3
3
  module Common
4
4
  # Kubernetes Entity List
5
5
  class EntityList < DelegateClass(Array)
6
- attr_reader :kind, :resourceVersion
6
+ attr_reader :continue, :kind, :resourceVersion
7
7
 
8
- def initialize(kind, resource_version, list)
8
+ def initialize(kind, resource_version, list, continue = nil)
9
9
  @kind = kind
10
10
  # rubocop:disable Style/VariableName
11
11
  @resourceVersion = resource_version
12
+ @continue = continue
12
13
  super(list)
13
14
  end
15
+
16
+ def last?
17
+ continue.nil?
18
+ end
14
19
  end
15
20
  end
16
21
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # An exec-based client auth provide
5
+ # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuration
6
+ # Inspired by https://github.com/kubernetes/client-go/blob/master/plugin/pkg/client/auth/exec/exec.go
7
+ class ExecCredentials
8
+ class << self
9
+ def run(opts)
10
+ require 'open3'
11
+ require 'json'
12
+
13
+ raise ArgumentError, 'exec options are required' if opts.nil?
14
+
15
+ cmd = opts['command']
16
+ args = opts['args']
17
+ env = map_env(opts['env'])
18
+
19
+ # Validate exec options
20
+ validate_opts(opts)
21
+
22
+ out, err, st = Open3.capture3(env, cmd, *args)
23
+
24
+ raise "exec command failed: #{err}" unless st.success?
25
+
26
+ creds = JSON.parse(out)
27
+ validate_credentials(opts, creds)
28
+ creds['status']
29
+ end
30
+
31
+ private
32
+
33
+ def validate_opts(opts)
34
+ raise KeyError, 'exec command is required' unless opts['command']
35
+ end
36
+
37
+ def validate_client_credentials_status(status)
38
+ has_client_cert_data = status.key?('clientCertificateData')
39
+ has_client_key_data = status.key?('clientKeyData')
40
+
41
+ if has_client_cert_data && !has_client_key_data
42
+ raise 'exec plugin didn\'t return client key data'
43
+ end
44
+
45
+ if !has_client_cert_data && has_client_key_data
46
+ raise 'exec plugin didn\'t return client certificate data'
47
+ end
48
+
49
+ has_client_cert_data && has_client_key_data
50
+ end
51
+
52
+ def validate_credentials_status(status)
53
+ raise 'exec plugin didn\'t return a status field' if status.nil?
54
+
55
+ has_client_credentials = validate_client_credentials_status(status)
56
+ has_token = status.key?('token')
57
+
58
+ if has_client_credentials && has_token
59
+ raise 'exec plugin returned both token and client data'
60
+ end
61
+
62
+ return if has_client_credentials || has_token
63
+
64
+ raise 'exec plugin didn\'t return a token or client data' unless has_token
65
+ end
66
+
67
+ def validate_credentials(opts, creds)
68
+ # out should have ExecCredential structure
69
+ raise 'invalid credentials' if creds.nil?
70
+
71
+ # Verify apiVersion?
72
+ api_version = opts['apiVersion']
73
+ if api_version && api_version != creds['apiVersion']
74
+ raise "exec plugin is configured to use API version #{api_version}, " \
75
+ "plugin returned version #{creds['apiVersion']}"
76
+ end
77
+
78
+ validate_credentials_status(creds['status'])
79
+ end
80
+
81
+ # Transform name/value pairs to hash
82
+ def map_env(env)
83
+ return {} unless env
84
+
85
+ Hash[env.map { |e| [e['name'], e['value']] }]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kubeclient/google_application_default_credentials'
4
+ require 'kubeclient/gcp_command_credentials'
5
+
6
+ module Kubeclient
7
+ # Handle different ways to get a bearer token for Google Cloud Platform.
8
+ class GCPAuthProvider
9
+ class << self
10
+ def token(config)
11
+ if config.key?('cmd-path')
12
+ Kubeclient::GCPCommandCredentials.token(config)
13
+ else
14
+ Kubeclient::GoogleApplicationDefaultCredentials.token
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Generates a bearer token for Google Cloud Platform.
5
+ class GCPCommandCredentials
6
+ class << self
7
+ def token(config)
8
+ require 'open3'
9
+ require 'shellwords'
10
+ require 'json'
11
+ require 'jsonpath'
12
+
13
+ cmd = config['cmd-path']
14
+ args = config['cmd-args']
15
+ token_key = config['token-key']
16
+
17
+ out, err, st = Open3.capture3(cmd, *args.split(' '))
18
+
19
+ raise "exec command failed: #{err}" unless st.success?
20
+
21
+ extract_token(out, token_key)
22
+ end
23
+
24
+ private
25
+
26
+ def extract_token(output, token_key)
27
+ JsonPath.on(output, token_key.gsub(/^{|}$/, '')).first
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Get a bearer token from the Google's application default credentials.
5
+ class GoogleApplicationDefaultCredentials
6
+ class GoogleDependencyError < LoadError # rubocop:disable Lint/InheritException
7
+ end
8
+
9
+ class << self
10
+ def token
11
+ begin
12
+ require 'googleauth'
13
+ rescue LoadError => e
14
+ raise GoogleDependencyError,
15
+ 'Error requiring googleauth gem. Kubeclient itself does not include the ' \
16
+ 'googleauth gem. To support auth-provider gcp, you must include it in your ' \
17
+ "calling application. Failed with: #{e.message}"
18
+ end
19
+
20
+ scopes = [
21
+ 'https://www.googleapis.com/auth/cloud-platform',
22
+ 'https://www.googleapis.com/auth/userinfo.email'
23
+ ]
24
+
25
+ authorization = Google::Auth.get_application_default(scopes)
26
+ authorization.apply({})
27
+ authorization.access_token
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # TODO: remove this on next major version bump
2
+ # Deprected http exception
3
+ class KubeException < StandardError
4
+ attr_reader :error_code, :message, :response
5
+
6
+ def initialize(error_code, message, response)
7
+ @error_code = error_code
8
+ @message = message
9
+ @response = response
10
+ end
11
+
12
+ def to_s
13
+ string = "HTTP status code #{@error_code}, #{@message}"
14
+ if @response.is_a?(RestClient::Response) && @response.request
15
+ string << " for #{@response.request.method.upcase} #{@response.request.url}"
16
+ end
17
+ string
18
+ end
19
+ end
20
+
21
+ module Kubeclient
22
+ # Exception that is raised when a http request fails
23
+ class HttpError < KubeException
24
+ end
25
+ end
@@ -0,0 +1,68 @@
1
+ module Kubeclient
2
+ module Common
3
+ # Backward compatibility for old versions where kind is missing (e.g. OpenShift Enterprise 3.1)
4
+ class MissingKindCompatibility
5
+ MAPPING = {
6
+ 'bindings' => 'Binding',
7
+ 'componentstatuses' => 'ComponentStatus',
8
+ 'endpoints' => 'Endpoints',
9
+ 'events' => 'Event',
10
+ 'limitranges' => 'LimitRange',
11
+ 'namespaces' => 'Namespace',
12
+ 'nodes' => 'Node',
13
+ 'persistentvolumeclaims' => 'PersistentVolumeClaim',
14
+ 'persistentvolumes' => 'PersistentVolume',
15
+ 'pods' => 'Pod',
16
+ 'podtemplates' => 'PodTemplate',
17
+ 'replicationcontrollers' => 'ReplicationController',
18
+ 'resourcequotas' => 'ResourceQuota',
19
+ 'secrets' => 'Secret',
20
+ 'securitycontextconstraints' => 'SecurityContextConstraints',
21
+ 'serviceaccounts' => 'ServiceAccount',
22
+ 'services' => 'Service',
23
+ 'buildconfigs' => 'BuildConfig',
24
+ 'builds' => 'Build',
25
+ 'clusternetworks' => 'ClusterNetwork',
26
+ 'clusterpolicies' => 'ClusterPolicy',
27
+ 'clusterpolicybindings' => 'ClusterPolicyBinding',
28
+ 'clusterrolebindings' => 'ClusterRoleBinding',
29
+ 'clusterroles' => 'ClusterRole',
30
+ 'deploymentconfigrollbacks' => 'DeploymentConfigRollback',
31
+ 'deploymentconfigs' => 'DeploymentConfig',
32
+ 'generatedeploymentconfigs' => 'DeploymentConfig',
33
+ 'groups' => 'Group',
34
+ 'hostsubnets' => 'HostSubnet',
35
+ 'identities' => 'Identity',
36
+ 'images' => 'Image',
37
+ 'imagestreamimages' => 'ImageStreamImage',
38
+ 'imagestreammappings' => 'ImageStreamMapping',
39
+ 'imagestreams' => 'ImageStream',
40
+ 'imagestreamtags' => 'ImageStreamTag',
41
+ 'localresourceaccessreviews' => 'LocalResourceAccessReview',
42
+ 'localsubjectaccessreviews' => 'LocalSubjectAccessReview',
43
+ 'netnamespaces' => 'NetNamespace',
44
+ 'oauthaccesstokens' => 'OAuthAccessToken',
45
+ 'oauthauthorizetokens' => 'OAuthAuthorizeToken',
46
+ 'oauthclientauthorizations' => 'OAuthClientAuthorization',
47
+ 'oauthclients' => 'OAuthClient',
48
+ 'policies' => 'Policy',
49
+ 'policybindings' => 'PolicyBinding',
50
+ 'processedtemplates' => 'Template',
51
+ 'projectrequests' => 'ProjectRequest',
52
+ 'projects' => 'Project',
53
+ 'resourceaccessreviews' => 'ResourceAccessReview',
54
+ 'rolebindings' => 'RoleBinding',
55
+ 'roles' => 'Role',
56
+ 'routes' => 'Route',
57
+ 'subjectaccessreviews' => 'SubjectAccessReview',
58
+ 'templates' => 'Template',
59
+ 'useridentitymappings' => 'UserIdentityMapping',
60
+ 'users' => 'User'
61
+ }.freeze
62
+
63
+ def self.resource_kind(name)
64
+ MAPPING[name]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kubeclient
4
+ # Uses OIDC id-tokens and refreshes them if they are stale.
5
+ class OIDCAuthProvider
6
+ class OpenIDConnectDependencyError < LoadError # rubocop:disable Lint/InheritException
7
+ end
8
+
9
+ class << self
10
+ def token(provider_config)
11
+ begin
12
+ require 'openid_connect'
13
+ rescue LoadError => e
14
+ raise OpenIDConnectDependencyError,
15
+ 'Error requiring openid_connect gem. Kubeclient itself does not include the ' \
16
+ 'openid_connect gem. To support auth-provider oidc, you must include it in your ' \
17
+ "calling application. Failed with: #{e.message}"
18
+ end
19
+
20
+ issuer_url = provider_config['idp-issuer-url']
21
+ discovery = OpenIDConnect::Discovery::Provider::Config.discover! issuer_url
22
+
23
+ if provider_config.key? 'id-token'
24
+ return provider_config['id-token'] unless expired?(provider_config['id-token'], discovery)
25
+ end
26
+
27
+ client = OpenIDConnect::Client.new(
28
+ identifier: provider_config['client-id'],
29
+ secret: provider_config['client-secret'],
30
+ authorization_endpoint: discovery.authorization_endpoint,
31
+ token_endpoint: discovery.token_endpoint,
32
+ userinfo_endpoint: discovery.userinfo_endpoint
33
+ )
34
+ client.refresh_token = provider_config['refresh-token']
35
+ client.access_token!.id_token
36
+ end
37
+
38
+ def expired?(id_token, discovery)
39
+ decoded_token = OpenIDConnect::ResponseObject::IdToken.decode(
40
+ id_token,
41
+ discovery.jwks
42
+ )
43
+ # If token expired or expiring within 60 seconds
44
+ Time.now.to_i + 60 > decoded_token.exp.to_i
45
+ rescue JSON::JWK::Set::KidNotFound
46
+ # Token cannot be verified: the kid it was signed with is not available for discovery
47
+ # Consider it expired and fetch a new one.
48
+ true
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,11 @@
1
+ require 'recursive_open_struct'
2
+
3
+ module Kubeclient
4
+ # Represents all the objects returned by Kubeclient
5
+ class Resource < RecursiveOpenStruct
6
+ def initialize(hash = nil, args = {})
7
+ args[:recurse_over_arrays] = true
8
+ super(hash, args)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module Kubeclient
2
+ class ResourceNotFoundError < HttpError
3
+ end
4
+ end
@@ -1,4 +1,4 @@
1
1
  # Kubernetes REST-API Client
2
2
  module Kubeclient
3
- VERSION = '0.3.0'
3
+ VERSION = '4.9.2'.freeze
4
4
  end