k8s-client-renewed 0.10.5.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.relaxed.yml +176 -0
- data/.rubocop.yml +57 -0
- data/.travis.yml +20 -0
- data/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +233 -0
- data/Rakefile +16 -0
- data/docker-compose.yaml +10 -0
- data/k8s-client.gemspec +39 -0
- data/lib/k8s-client.rb +3 -0
- data/lib/k8s/api_client.rb +115 -0
- data/lib/k8s/client.rb +282 -0
- data/lib/k8s/client/version.rb +8 -0
- data/lib/k8s/config.rb +168 -0
- data/lib/k8s/error.rb +66 -0
- data/lib/k8s/json_parser.rb +97 -0
- data/lib/k8s/json_parser/yajl.rb +44 -0
- data/lib/k8s/logging.rb +87 -0
- data/lib/k8s/resource.rb +111 -0
- data/lib/k8s/resource_client.rb +349 -0
- data/lib/k8s/stack.rb +187 -0
- data/lib/k8s/transport.rb +380 -0
- data/lib/k8s/util.rb +178 -0
- data/lib/k8s/watch_event.rb +12 -0
- metadata +224 -0
data/README.md
ADDED
@@ -0,0 +1,233 @@
|
|
1
|
+
# K8s::Client
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.com/kontena/k8s-client.svg?branch=master)](https://travis-ci.com/kontena/k8s-client)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/k8s-client.svg)](https://badge.fury.io/rb/k8s-client)
|
5
|
+
[![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/kontena/k8s-client/master)
|
6
|
+
|
7
|
+
|
8
|
+
Ruby client library for the Kubernetes (1.9+) API
|
9
|
+
|
10
|
+
## Highlights
|
11
|
+
|
12
|
+
* Clean API for dynamic Kubernetes API Groups / Resources
|
13
|
+
* Fast API requests using HTTP connection keepalive
|
14
|
+
* Fast API discovery and resource listings using pipelined HTTP requests
|
15
|
+
* Typed errors with useful debugging information
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'k8s-client'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install k8s-client
|
32
|
+
|
33
|
+
And then load the code using:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
require 'k8s-client'
|
37
|
+
```
|
38
|
+
|
39
|
+
To use the [yajl-ruby](https://github.com/brianmario/yajl-ruby) JSON parser backend, add `yalj-ruby` to your Gemfile:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
gem 'yajl-ruby'
|
43
|
+
gem 'k8s-client'
|
44
|
+
```
|
45
|
+
|
46
|
+
And then load the code using:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'k8s-client'
|
50
|
+
require 'k8s/json_parser/yajl'
|
51
|
+
```
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
### Overview
|
56
|
+
The top-level `K8s::Client` provides access to separate `APIClient` instances for each Kubernetes API Group (`v1`, `apps/v1`, etc.), which in turns provides access to separate `ResourceClient` instances for each API resource type (`nodes`, `pods`, `deployments`, etc.).
|
57
|
+
|
58
|
+
Individual resources are returned as `K8s::Resource` instances, which provide attribute access (`resource.metadata.name`). The resource instances are returned by methods such as `client.api('v1').resource('nodes').get('foo')`, and passed as arguments for `client.api('v1').resource('nodes').create_resource(res)`. Resources can also be loaded from disk using `K8s::Resource.from_files(path)`, and passed to the top-level methods such as `client.create_resource(res)`, which lookup the correct API/Resource client from the resource `apiVersion` and `kind`.
|
59
|
+
|
60
|
+
The different `K8s::Error::API` subclasses represent different HTTP response codes, such as `K8s::Error::NotFound` or `K8s::Error::Conflict`.
|
61
|
+
|
62
|
+
### Creating a client
|
63
|
+
|
64
|
+
#### Unauthenticated client
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
client = K8s.client('https://localhost:6443', ssl_verify_peer: false)
|
68
|
+
```
|
69
|
+
|
70
|
+
The keyword options are [Excon](https://github.com/excon/excon/) options.
|
71
|
+
|
72
|
+
#### Client from kubeconfig
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
client = K8s::Client.config(
|
76
|
+
K8s::Config.load_file(
|
77
|
+
File.expand_path '~/.kube/config'
|
78
|
+
)
|
79
|
+
)
|
80
|
+
```
|
81
|
+
|
82
|
+
#### Supported kubeconfig options
|
83
|
+
|
84
|
+
Not all kubeconfig options are supported, only the following kubeconfig options work:
|
85
|
+
|
86
|
+
* `current-context`
|
87
|
+
* `context.cluster`
|
88
|
+
* `context.user`
|
89
|
+
* `cluster.server`
|
90
|
+
* `cluster.insecure_skip_tls_verify`
|
91
|
+
* `cluster.certificate_authority`
|
92
|
+
* `cluster.certificate_authority_data`
|
93
|
+
* `user.client_certificate` + `user.client_key`
|
94
|
+
* `user.client_certificate_data` + `user.client_key_data`
|
95
|
+
* `user.token`
|
96
|
+
|
97
|
+
##### With overrides
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
client = K8s::Client.config(K8s::Config.load_file('~/.kube/config'),
|
101
|
+
server: 'http://localhost:8001',
|
102
|
+
)
|
103
|
+
```
|
104
|
+
|
105
|
+
#### In-cluster client from pod envs/secrets
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
client = K8s::Client.in_cluster_config
|
109
|
+
```
|
110
|
+
|
111
|
+
### Logging
|
112
|
+
|
113
|
+
#### Quiet
|
114
|
+
|
115
|
+
To supress any warning messages:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
K8s::Logging.quiet!
|
119
|
+
K8s::Transport.quiet!
|
120
|
+
```
|
121
|
+
|
122
|
+
The `K8s::Transport` is quiet by default, but other components may log warnings in the future.
|
123
|
+
|
124
|
+
#### Debugging
|
125
|
+
|
126
|
+
Log all API requests
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
K8s::Logging.debug!
|
130
|
+
K8s::Transport.verbose!
|
131
|
+
```
|
132
|
+
|
133
|
+
```
|
134
|
+
I, [2018-08-09T14:19:50.404739 #1] INFO -- K8s::Transport: Using config with server=https://167.99.39.233:6443
|
135
|
+
I, [2018-08-09T14:19:50.629521 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /version => HTTP 200: <K8s::Resource> in 0.224s
|
136
|
+
I, [2018-08-09T14:19:50.681367 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /api/v1 => HTTP 200: <K8s::Resource> in 0.046s
|
137
|
+
I, [2018-08-09T14:19:51.018740 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /api/v1/pods => HTTP 200: <K8s::Resource> in 0.316s
|
138
|
+
```
|
139
|
+
|
140
|
+
Using `K8s::Transport.debug!` will also log request/response bodies. The `EXCON_DEBUG=true` env will log all request/response attributes, including headers.
|
141
|
+
|
142
|
+
### Prefetching API resources
|
143
|
+
|
144
|
+
Operations like mapping a resource `kind` to an API resource URL require knowledge of the API resource lists for the API group. Mapping resources for multiple API groups would require fetching the API resource lists for each API group in turn, leading to additional request latency. This can be optimized using resource prefetching:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
client.apis(prefetch_resources: true)
|
148
|
+
```
|
149
|
+
|
150
|
+
This will fetch the API resource lists for all API groups in a single pipelined request.
|
151
|
+
|
152
|
+
### Listing resources
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
client.api('v1').resource('pods', namespace: 'default').list(labelSelector: {'role' => 'test'}).each do |pod|
|
156
|
+
puts "namespace=#{pod.metadata.namespace} pod: #{pod.metadata.name} node=#{pod.spec.nodeName}"
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
### Updating resources
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
node = client.api('v1').resource('nodes').get('test-node')
|
164
|
+
|
165
|
+
node[:spec][:unschedulable] = true
|
166
|
+
|
167
|
+
client.api('v1').resource('nodes').update_resource(node)
|
168
|
+
```
|
169
|
+
|
170
|
+
### Deleting resources
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
pod = client.api('v1').resource('pods', namespace: 'default').delete('test-pod')
|
174
|
+
```
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
pods = client.api('v1').resource('pods', namespace: 'default').delete_collection(labelSelector: {'role' => 'test'})
|
178
|
+
```
|
179
|
+
|
180
|
+
### Creating resources
|
181
|
+
|
182
|
+
#### Programmatically defined resources
|
183
|
+
```ruby
|
184
|
+
service = K8s::Resource.new(
|
185
|
+
apiVersion: 'v1',
|
186
|
+
kind: 'Service',
|
187
|
+
metadata: {
|
188
|
+
namespace: 'default',
|
189
|
+
name: 'test',
|
190
|
+
},
|
191
|
+
spec: {
|
192
|
+
type: 'ClusterIP',
|
193
|
+
ports: [
|
194
|
+
{ port: 80 },
|
195
|
+
],
|
196
|
+
selector: {'app' => 'test'},
|
197
|
+
},
|
198
|
+
)
|
199
|
+
|
200
|
+
logger.info "Create service=#{service.metadata.name} in namespace=#{service.metadata.namespace}"
|
201
|
+
|
202
|
+
service = client.api('v1').resource('services').create_resource(service)
|
203
|
+
```
|
204
|
+
|
205
|
+
#### From file(s)
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
resources = K8s::Resource.from_files('./test.yaml')
|
209
|
+
|
210
|
+
for resource in resources
|
211
|
+
resource = client.create_resource(resource)
|
212
|
+
end
|
213
|
+
```
|
214
|
+
|
215
|
+
### Patching resources
|
216
|
+
|
217
|
+
```ruby
|
218
|
+
client.api('apps/v1').resource('deployments', namespace: 'default').merge_patch('test', {
|
219
|
+
spec: { replicas: 3 },
|
220
|
+
})
|
221
|
+
```
|
222
|
+
|
223
|
+
### Watching resources
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
client.api('v1').resource('pods', namespace: 'default').watch(labelSelector: {'role' => 'test'}) do |watch_event|
|
227
|
+
puts "type=#{watch_event.type} pod=#{watch_event.resource.metadata.name}"
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
## Contributing
|
232
|
+
|
233
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kontena/k8s-client.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
RuboCop::RakeTask.new(:rubocop).tap do |task|
|
10
|
+
task.options = %w(--fail-level A -S --format c)
|
11
|
+
if RUBY_ENGINE == 'ruby' && RbConfig::CONFIG['host_os'] !~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
12
|
+
task.options << '--parallel'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
task default: [:spec, :rubocop]
|
data/docker-compose.yaml
ADDED
data/k8s-client.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "k8s/client/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "k8s-client-renewed"
|
8
|
+
spec.version = K8s::Client::VERSION
|
9
|
+
spec.authors = ["Kontena, Inc."]
|
10
|
+
spec.email = ["info@kontena.io"]
|
11
|
+
spec.license = "Apache-2.0"
|
12
|
+
|
13
|
+
spec.summary = "Kubernetes client library"
|
14
|
+
spec.homepage = "https://github.com/kontena/k8s-client"
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
spec.bindir = "bin"
|
22
|
+
spec.executables = []
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
# spec.required_ruby_version = '> 2.4'
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "excon"#, "~> 0.66"
|
27
|
+
spec.add_runtime_dependency "recursive-open-struct"#, "~> 1.1.0"
|
28
|
+
spec.add_runtime_dependency 'hashdiff'#, '~> 1.0.0'
|
29
|
+
spec.add_runtime_dependency 'jsonpath'#, '~> 0.9.5'
|
30
|
+
spec.add_runtime_dependency "yaml-safe_load_stream-renewed"#, "~> 0.1"
|
31
|
+
|
32
|
+
# spec.add_development_dependency "bundler"#, ">= 1.17", "< 3.0"
|
33
|
+
spec.add_development_dependency "byebug"#, "~> 10.0"
|
34
|
+
spec.add_development_dependency "rake"#, "~> 10.0"
|
35
|
+
spec.add_development_dependency "rspec"#, "~> 3.7"
|
36
|
+
spec.add_development_dependency "webmock"#, "~> 3.6.2"
|
37
|
+
spec.add_development_dependency "rubocop"#, "~> 0.59"
|
38
|
+
spec.add_development_dependency 'yajl-ruby'#, '~> 1.4.0'
|
39
|
+
end
|
data/lib/k8s-client.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
# Per-APIGroup/version client.
|
5
|
+
#
|
6
|
+
# Offers access to {ResourceClient} instances for the APIResource types defined in this apigroup/version
|
7
|
+
class APIClient
|
8
|
+
# @param api_version [String] either core version (v1) or apigroup/apiversion (apps/v1)
|
9
|
+
# @return [String]
|
10
|
+
def self.path(api_version)
|
11
|
+
if api_version.include? '/'
|
12
|
+
File.join('/apis', api_version)
|
13
|
+
else
|
14
|
+
File.join('/api', api_version)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param transport [K8s::Transport]
|
19
|
+
# @param api_version [String] "group/version" or "version" (core)
|
20
|
+
def initialize(transport, api_version)
|
21
|
+
@transport = transport
|
22
|
+
@api_version = api_version
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
attr_reader :api_version
|
27
|
+
|
28
|
+
# @param path [Array<String>] join path from parts
|
29
|
+
# @return [String]
|
30
|
+
def path(*path)
|
31
|
+
@transport.path(self.class.path(@api_version), *path)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Bool] loaded yet?
|
35
|
+
def api_resources?
|
36
|
+
!!@api_resources
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param api_resources [Array<K8s::Resource>]
|
40
|
+
attr_writer :api_resources
|
41
|
+
|
42
|
+
# Force-update APIResources
|
43
|
+
#
|
44
|
+
# @return [Array<K8s::Resource>]
|
45
|
+
def api_resources!
|
46
|
+
@api_resources = @transport.get(path).resources
|
47
|
+
end
|
48
|
+
|
49
|
+
# Cached APIResources
|
50
|
+
#
|
51
|
+
# @return [Array<K8s::Resource>]
|
52
|
+
def api_resources
|
53
|
+
@api_resources || api_resources!
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param resource_name [String]
|
57
|
+
# @raise [K8s::Error::UndefinedResource]
|
58
|
+
# @return [K8s::Resource]
|
59
|
+
def find_api_resource(resource_name)
|
60
|
+
found_resource = api_resources.find{ |api_resource| api_resource.name == resource_name }
|
61
|
+
found_resource ||= api_resources!.find{ |api_resource| api_resource.name == resource_name }
|
62
|
+
raise K8s::Error::UndefinedResource, "Unknown resource #{resource_name} for #{@api_version}" unless found_resource
|
63
|
+
|
64
|
+
found_resource
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param resource_name [String]
|
68
|
+
# @param namespace [String, nil]
|
69
|
+
# @raise [K8s::Error::UndefinedResource]
|
70
|
+
# @return [K8s::ResourceClient]
|
71
|
+
def resource(resource_name, namespace: nil)
|
72
|
+
ResourceClient.new(@transport, self, find_api_resource(resource_name), namespace: namespace)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param resource [K8s::Resource]
|
76
|
+
# @param namespace [String, nil] default if resource is missing namespace
|
77
|
+
# @raise [K8s::Error::UndefinedResource]
|
78
|
+
# @return [K8s::ResourceClient]
|
79
|
+
def client_for_resource(resource, namespace: nil)
|
80
|
+
unless @api_version == resource.apiVersion
|
81
|
+
raise K8s::Error::UndefinedResource, "Invalid apiVersion=#{resource.apiVersion} for #{@api_version} client"
|
82
|
+
end
|
83
|
+
|
84
|
+
found_resource = api_resources.find{ |api_resource| api_resource.kind == resource.kind }
|
85
|
+
found_resource ||= api_resources!.find{ |api_resource| api_resource.kind == resource.kind }
|
86
|
+
raise K8s::Error::UndefinedResource, "Unknown resource kind=#{resource.kind} for #{@api_version}" unless found_resource
|
87
|
+
|
88
|
+
ResourceClient.new(@transport, self, found_resource, namespace: resource.metadata.namespace || namespace)
|
89
|
+
end
|
90
|
+
|
91
|
+
# TODO: skip non-namespaced resources if namespace is given, or ignore namespace?
|
92
|
+
#
|
93
|
+
# @param namespace [String, nil]
|
94
|
+
# @return [Array<K8s::ResourceClient>]
|
95
|
+
def resources(namespace: nil)
|
96
|
+
api_resources.map{ |api_resource|
|
97
|
+
ResourceClient.new(@transport, self, api_resource,
|
98
|
+
namespace: namespace)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Pipeline list requests for multiple resource types.
|
103
|
+
#
|
104
|
+
# Returns flattened array with mixed resource kinds.
|
105
|
+
#
|
106
|
+
# @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
|
107
|
+
# @param options @see [K8s::ResourceClient#list]
|
108
|
+
# @return [Array<K8s::Resource>]
|
109
|
+
def list_resources(resources = nil, **options)
|
110
|
+
resources ||= self.resources.select(&:list?)
|
111
|
+
|
112
|
+
ResourceClient.list(resources, @transport, **options)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/k8s/client.rb
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
require 'monitor'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
require 'k8s/json_parser'
|
10
|
+
require 'k8s/util'
|
11
|
+
|
12
|
+
require 'k8s/config'
|
13
|
+
require 'k8s/logging'
|
14
|
+
require 'k8s/api_client'
|
15
|
+
require "k8s/error"
|
16
|
+
require 'k8s/resource'
|
17
|
+
require 'k8s/resource_client'
|
18
|
+
require 'k8s/stack'
|
19
|
+
require 'k8s/transport'
|
20
|
+
|
21
|
+
module K8s
|
22
|
+
# @param server [String] http/s URL
|
23
|
+
# @param options [Hash] @see Transport.new
|
24
|
+
# @return [K8s::Client]
|
25
|
+
def self.client(server, **options)
|
26
|
+
Client.new(Transport.new(server, **options))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Top-level client wrapper.
|
30
|
+
# Uses a {Transport} instance to talk to the kube API.
|
31
|
+
# Offers access to {APIClient} and {ResourceClient} instances.
|
32
|
+
class Client
|
33
|
+
# @param config [Phraos::Kube::Config]
|
34
|
+
# @param namespace [String] @see #initialize
|
35
|
+
# @param options [Hash] @see Transport.config
|
36
|
+
# @return [K8s::Client]
|
37
|
+
def self.config(config, namespace: nil, **options)
|
38
|
+
new(
|
39
|
+
Transport.config(config, **options),
|
40
|
+
namespace: namespace
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
# An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
|
45
|
+
# @see K8s::Transport#in_cluster_config
|
46
|
+
#
|
47
|
+
# @param namespace [String] default namespace for all operations
|
48
|
+
# @param options [Hash] options passed to transport, @see Transport#in_cluster_config
|
49
|
+
# @return [K8s::Client]
|
50
|
+
# @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
|
51
|
+
def self.in_cluster_config(namespace: nil, **options)
|
52
|
+
new(Transport.in_cluster_config(**options), namespace: namespace)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Attempts to create a K8s::Client instance automatically using environment variables, existing configuration
|
56
|
+
# files or in cluster configuration.
|
57
|
+
#
|
58
|
+
# Look-up order:
|
59
|
+
# - KUBE_TOKEN, KUBE_CA, KUBE_SERVER environment variables
|
60
|
+
# - KUBECONFIG environment variable
|
61
|
+
# - $HOME/.kube/config file
|
62
|
+
# - In cluster configuration
|
63
|
+
#
|
64
|
+
# Will raise when no means of configuration is available
|
65
|
+
#
|
66
|
+
# @param options [Hash] default namespace for all operations
|
67
|
+
# @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
|
68
|
+
# @return [K8s::Client]
|
69
|
+
def self.autoconfig(namespace: nil, **options)
|
70
|
+
if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
|
71
|
+
unless Base64.decode64(ENV['KUBE_CA']).match?(/CERTIFICATE/)
|
72
|
+
raise ArgumentError, 'KUBE_CA does not seem to be base64 encoded'
|
73
|
+
end
|
74
|
+
|
75
|
+
begin
|
76
|
+
token = options[:auth_token] || Base64.strict_decode64(ENV['KUBE_TOKEN'])
|
77
|
+
rescue ArgumentError
|
78
|
+
raise ArgumentError, 'KUBE_TOKEN does not seem to be base64 encoded'
|
79
|
+
end
|
80
|
+
|
81
|
+
configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: token)
|
82
|
+
elsif !ENV['KUBECONFIG'].to_s.empty?
|
83
|
+
configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'])
|
84
|
+
else
|
85
|
+
found_config = [
|
86
|
+
File.join(Dir.home, '.kube', 'config'),
|
87
|
+
'/etc/kubernetes/admin.conf',
|
88
|
+
'/etc/kubernetes/kubelet.conf'
|
89
|
+
].find { |f| File.exist?(f) && File.readable?(f) }
|
90
|
+
|
91
|
+
configuration = K8s::Config.load_file(found_config) if found_config
|
92
|
+
end
|
93
|
+
|
94
|
+
if configuration
|
95
|
+
config(configuration, namespace: namespace, **options)
|
96
|
+
else
|
97
|
+
in_cluster_config(namespace: namespace, **options)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
include MonitorMixin
|
102
|
+
|
103
|
+
attr_reader :transport
|
104
|
+
|
105
|
+
# @param transport [K8s::Transport]
|
106
|
+
# @param namespace [String, nil] default namespace for all operations
|
107
|
+
def initialize(transport, namespace: nil)
|
108
|
+
@transport = transport
|
109
|
+
@namespace = namespace
|
110
|
+
|
111
|
+
@api_clients = {}
|
112
|
+
super()
|
113
|
+
end
|
114
|
+
|
115
|
+
# @raise [K8s::Error]
|
116
|
+
# @return [K8s::Resource]
|
117
|
+
def version
|
118
|
+
@version ||= @transport.version
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param api_version [String] "group/version" or "version" (core)
|
122
|
+
# @return [APIClient]
|
123
|
+
def api(api_version = 'v1')
|
124
|
+
@api_clients[api_version] ||= APIClient.new(@transport, api_version)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Force-update /apis cache.
|
128
|
+
# Required if creating new CRDs/apiservices.
|
129
|
+
#
|
130
|
+
# @return [Array<String>]
|
131
|
+
def api_groups!
|
132
|
+
synchronize do
|
133
|
+
@api_groups = @transport.get(
|
134
|
+
'/apis',
|
135
|
+
response_class: K8s::Resource
|
136
|
+
).groups.flat_map{ |api_group| api_group.versions.map(&:groupVersion) }
|
137
|
+
|
138
|
+
@api_clients.clear
|
139
|
+
end
|
140
|
+
|
141
|
+
@api_groups
|
142
|
+
end
|
143
|
+
|
144
|
+
# Cached /apis preferred group apiVersions
|
145
|
+
# @return [Array<String>]
|
146
|
+
def api_groups
|
147
|
+
@api_groups || api_groups!
|
148
|
+
end
|
149
|
+
|
150
|
+
# @param api_versions [Array<String>] defaults to all APIs
|
151
|
+
# @param prefetch_resources [Boolean] prefetch any missing api_resources for each api_version
|
152
|
+
# @param skip_missing [Boolean] return APIClient without api_resources? if 404
|
153
|
+
# @return [Array<APIClient>]
|
154
|
+
def apis(api_versions = nil, prefetch_resources: false, skip_missing: false)
|
155
|
+
api_versions ||= ['v1'] + api_groups
|
156
|
+
|
157
|
+
if prefetch_resources
|
158
|
+
# api groups that are missing their api_resources
|
159
|
+
api_paths = api_versions
|
160
|
+
.uniq
|
161
|
+
.reject{ |api_version| api(api_version).api_resources? }
|
162
|
+
.map{ |api_version| APIClient.path(api_version) }
|
163
|
+
|
164
|
+
# load into APIClient.api_resources=
|
165
|
+
begin
|
166
|
+
@transport.gets(*api_paths, skip_missing: skip_missing).each do |api_resource_list|
|
167
|
+
api(api_resource_list.groupVersion).api_resources = api_resource_list.resources if api_resource_list
|
168
|
+
end
|
169
|
+
rescue K8s::Error::NotFound, K8s::Error::ServiceUnavailable # rubocop:disable Lint/HandleExceptions
|
170
|
+
# kubernetes api is in unstable state
|
171
|
+
# because this is only performance optimization, better to skip prefetch and move on
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
api_versions.map{ |api_version| api(api_version) }
|
176
|
+
end
|
177
|
+
|
178
|
+
# @param namespace [String, nil]
|
179
|
+
# @return [Array<K8s::ResourceClient>]
|
180
|
+
def resources(namespace: nil)
|
181
|
+
apis(prefetch_resources: true).map { |api|
|
182
|
+
begin
|
183
|
+
api.resources(namespace: namespace)
|
184
|
+
rescue K8s::Error::ServiceUnavailable, K8s::Error::NotFound
|
185
|
+
[]
|
186
|
+
end
|
187
|
+
}.flatten
|
188
|
+
end
|
189
|
+
|
190
|
+
# Pipeline list requests for multiple resource types.
|
191
|
+
#
|
192
|
+
# Returns flattened array with mixed resource kinds.
|
193
|
+
#
|
194
|
+
# @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
|
195
|
+
# @param options @see K8s::ResourceClient#list
|
196
|
+
# @return [Array<K8s::Resource>]
|
197
|
+
def list_resources(resources = nil, **options)
|
198
|
+
cached_clients = @api_clients.size.positive?
|
199
|
+
resources ||= self.resources.select(&:list?)
|
200
|
+
|
201
|
+
begin
|
202
|
+
ResourceClient.list(resources, @transport, **options)
|
203
|
+
rescue K8s::Error::NotFound
|
204
|
+
raise unless cached_clients
|
205
|
+
|
206
|
+
cached_clients = false
|
207
|
+
api_groups!
|
208
|
+
retry
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# @param resource [K8s::Resource]
|
213
|
+
# @param namespace [String, nil] default if resource is missing namespace
|
214
|
+
# @raise [K8s::Error::NotFound] API Group does not exist
|
215
|
+
# @raise [K8s::Error::UndefinedResource]
|
216
|
+
# @return [K8s::ResourceClient]
|
217
|
+
def client_for_resource(resource, namespace: nil)
|
218
|
+
api(resource.apiVersion).client_for_resource(resource, namespace: namespace)
|
219
|
+
end
|
220
|
+
|
221
|
+
# @param resource [K8s::Resource]
|
222
|
+
# @return [K8s::Resource]
|
223
|
+
def create_resource(resource)
|
224
|
+
client_for_resource(resource).create_resource(resource)
|
225
|
+
end
|
226
|
+
|
227
|
+
# @param resource [K8s::Resource]
|
228
|
+
# @return [K8s::Resource]
|
229
|
+
def get_resource(resource)
|
230
|
+
client_for_resource(resource).get_resource(resource)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns nils for any resources that do not exist.
|
234
|
+
# This includes custom resources that were not yet defined.
|
235
|
+
#
|
236
|
+
# @param resources [Array<K8s::Resource>]
|
237
|
+
# @return [Array<K8s::Resource, nil>] matching resources array 1:1
|
238
|
+
def get_resources(resources)
|
239
|
+
# prefetch api resources, skip missing APIs
|
240
|
+
resource_apis = apis(resources.map(&:apiVersion), prefetch_resources: true, skip_missing: true)
|
241
|
+
|
242
|
+
# map each resource to excon request options, or nil if resource is not (yet) defined
|
243
|
+
requests = resources.zip(resource_apis).map{ |resource, api_client|
|
244
|
+
next nil unless api_client.api_resources?
|
245
|
+
|
246
|
+
resource_client = api_client.client_for_resource(resource)
|
247
|
+
|
248
|
+
{
|
249
|
+
method: 'GET',
|
250
|
+
path: resource_client.path(resource.metadata.name, namespace: resource.metadata.namespace),
|
251
|
+
response_class: resource_client.resource_class
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
# map non-nil requests to response objects, or nil for nil request options
|
256
|
+
Util.compact_map(requests) { |reqs|
|
257
|
+
@transport.requests(*reqs, skip_missing: true)
|
258
|
+
}
|
259
|
+
end
|
260
|
+
|
261
|
+
# @param resource [K8s::Resource]
|
262
|
+
# @return [K8s::Resource]
|
263
|
+
def update_resource(resource)
|
264
|
+
client_for_resource(resource).update_resource(resource)
|
265
|
+
end
|
266
|
+
|
267
|
+
# @param resource [K8s::Resource]
|
268
|
+
# @param options [Hash]
|
269
|
+
# @see ResourceClient#delete for options
|
270
|
+
# @return [K8s::Resource]
|
271
|
+
def delete_resource(resource, **options)
|
272
|
+
client_for_resource(resource).delete_resource(resource, **options)
|
273
|
+
end
|
274
|
+
|
275
|
+
# @param resource [K8s::Resource]
|
276
|
+
# @param attrs [Hash]
|
277
|
+
# @return [K8s::Client]
|
278
|
+
def patch_resource(resource, attrs)
|
279
|
+
client_for_resource(resource).json_patch(resource.metadata.name, attrs)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|