k8s-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +16 -0
- data/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/bin/k8s-client +301 -0
- data/docker-compose.yaml +10 -0
- data/k8s-client.gemspec +35 -0
- data/lib/k8s-client.rb +13 -0
- data/lib/k8s/api.rb +26 -0
- data/lib/k8s/api/metav1.rb +20 -0
- data/lib/k8s/api/metav1/api_group.rb +22 -0
- data/lib/k8s/api/metav1/api_resource.rb +24 -0
- data/lib/k8s/api/metav1/list.rb +18 -0
- data/lib/k8s/api/metav1/object.rb +51 -0
- data/lib/k8s/api/metav1/status.rb +31 -0
- data/lib/k8s/api/version.rb +14 -0
- data/lib/k8s/api_client.rb +109 -0
- data/lib/k8s/client.rb +148 -0
- data/lib/k8s/client/version.rb +5 -0
- data/lib/k8s/config.rb +100 -0
- data/lib/k8s/error.rb +47 -0
- data/lib/k8s/logging.rb +58 -0
- data/lib/k8s/resource.rb +67 -0
- data/lib/k8s/resource_client.rb +259 -0
- data/lib/k8s/stack.rb +137 -0
- data/lib/k8s/transport.rb +251 -0
- metadata +186 -0
data/lib/k8s/stack.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module K8s
|
4
|
+
# Usage: customize the LABEL and CHECKSUM_ANNOTATION
|
5
|
+
class Stack
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
LABEL = 'k8s.kontena.io/stack'
|
9
|
+
CHECKSUM_ANNOTATION = 'k8s.kontena.io/stack-checksum'
|
10
|
+
PRUNE_IGNORE = [
|
11
|
+
'v1:ComponentStatus', # apiserver ignores GET /v1/componentstatuses?labelSelector=... and returns all resources
|
12
|
+
'v1:Endpoints', # inherits stack label from service, but not checksum annotation
|
13
|
+
]
|
14
|
+
|
15
|
+
# @param name [String] unique name for stack
|
16
|
+
# @param path [String] load resources from YAML files
|
17
|
+
# @return [K8s::Stack]
|
18
|
+
def self.load(name, path, **options)
|
19
|
+
resources = K8s::Resource.from_files(path)
|
20
|
+
new(name, resources, **options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param name [String] unique name for stack
|
24
|
+
# @param path [String] load resources from YAML files
|
25
|
+
# @param client [K8s::Client] apply using client
|
26
|
+
# @param prune [Boolean] delete old resources
|
27
|
+
def self.apply(name, path, client, prune: true, **options)
|
28
|
+
load(name, path, **options).apply(client, prune: prune)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Remove any installed stack resources.
|
32
|
+
#
|
33
|
+
# @param name [String] unique name for stack
|
34
|
+
# @param client [K8s::Client] apply using client
|
35
|
+
def self.delete(name, client, **options)
|
36
|
+
new(name, **options).delete(client)
|
37
|
+
end
|
38
|
+
|
39
|
+
attr_reader :name, :resources
|
40
|
+
|
41
|
+
def initialize(name, resources = [], debug: false, label: LABEL, checksum_annotation: CHECKSUM_ANNOTATION)
|
42
|
+
@name = name
|
43
|
+
@resources = resources
|
44
|
+
@keep_resources = {}
|
45
|
+
@label = label
|
46
|
+
@checksum_annotation = checksum_annotation
|
47
|
+
|
48
|
+
logger! progname: name, debug: debug
|
49
|
+
end
|
50
|
+
|
51
|
+
def checksum
|
52
|
+
@checksum ||= SecureRandom.hex(16)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param resource [K8s::Resource] to apply
|
56
|
+
# @param base_resource [K8s::Resource] preserve existing attributes from base resource
|
57
|
+
# @return [K8s::Resource]
|
58
|
+
def prepare_resource(resource, base_resource: nil)
|
59
|
+
if base_resource
|
60
|
+
resource = base_resource.merge(resource)
|
61
|
+
end
|
62
|
+
|
63
|
+
# add stack metadata
|
64
|
+
resource.merge(metadata: {
|
65
|
+
labels: { @label => name },
|
66
|
+
annotations: { @checksum_annotation => checksum },
|
67
|
+
})
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Array<K8s::Resource>]
|
71
|
+
def apply(client, prune: true)
|
72
|
+
server_resources = client.get_resources(resources)
|
73
|
+
|
74
|
+
resources.zip(server_resources).map do |resource, server_resource|
|
75
|
+
if server_resource
|
76
|
+
# keep server checksum for comparison
|
77
|
+
# NOTE: this will not compare equal for resources with arrays containing hashes with default values applied by the server
|
78
|
+
# however, that will just cause extra PUTs, so it doesn't have any functional effects
|
79
|
+
compare_resource = server_resource.merge(resource).merge(metadata: {
|
80
|
+
labels: { @label => name },
|
81
|
+
})
|
82
|
+
end
|
83
|
+
|
84
|
+
if !server_resource
|
85
|
+
logger.info "Create resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{checksum}"
|
86
|
+
keep_resource! client.create_resource(prepare_resource(resource))
|
87
|
+
elsif server_resource != compare_resource
|
88
|
+
logger.info "Update resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{checksum}"
|
89
|
+
keep_resource! client.update_resource(prepare_resource(resource, base_resource: server_resource))
|
90
|
+
else
|
91
|
+
logger.info "Keep resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{compare_resource.metadata.annotations[@checksum_annotation]}"
|
92
|
+
keep_resource! compare_resource
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
prune(client, keep_resources: true) if prune
|
97
|
+
end
|
98
|
+
|
99
|
+
# key MUST NOT include resource.apiVersion: the same kind can be aliased in different APIs
|
100
|
+
def keep_resource!(resource)
|
101
|
+
@keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] = resource.metadata.annotations[@checksum_annotation]
|
102
|
+
end
|
103
|
+
def keep_resource?(resource)
|
104
|
+
@keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] == resource.metadata.annotations[@checksum_annotation]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Delete all stack resources that were not applied
|
108
|
+
def prune(client, keep_resources: )
|
109
|
+
client.list_resources(labelSelector: {@label => name}).each do |resource|
|
110
|
+
next if PRUNE_IGNORE.include? "#{resource.apiVersion}:#{resource.kind}"
|
111
|
+
|
112
|
+
resource_label = resource.metadata.labels ? resource.metadata.labels[@label] : nil
|
113
|
+
resource_checksum = resource.metadata.annotations ? resource.metadata.annotations[@checksum_annotation] : nil
|
114
|
+
|
115
|
+
logger.debug { "List resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace} with checksum=#{resource_checksum}" }
|
116
|
+
|
117
|
+
if resource_label != name
|
118
|
+
# apiserver did not respect labelSelector
|
119
|
+
elsif keep_resources && keep_resource?(resource)
|
120
|
+
# resource is up-to-date
|
121
|
+
else
|
122
|
+
logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace}"
|
123
|
+
begin
|
124
|
+
client.delete_resource(resource)
|
125
|
+
rescue K8s::Error::NotFound
|
126
|
+
# assume aliased objects in multiple API groups, like for Deployments
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Delete all stack resources
|
133
|
+
def delete(client)
|
134
|
+
prune(client, keep_resources: false)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module K8s
|
5
|
+
class Transport
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
quiet! # do not log warnings by default
|
9
|
+
|
10
|
+
EXCON_MIDDLEWARES = [
|
11
|
+
# XXX: necessary? redirected requests omit authz headers?
|
12
|
+
Excon::Middleware::RedirectFollower,
|
13
|
+
] + Excon.defaults[:middlewares]
|
14
|
+
|
15
|
+
REQUEST_HEADERS = {
|
16
|
+
'Accept' => 'application/json',
|
17
|
+
}
|
18
|
+
|
19
|
+
# Construct transport from kubeconfig
|
20
|
+
#
|
21
|
+
# @param config [Phraos::Kube::Config]
|
22
|
+
# @return [K8s::Transport]
|
23
|
+
def self.config(config)
|
24
|
+
options = {}
|
25
|
+
|
26
|
+
if config.cluster.insecure_skip_tls_verify
|
27
|
+
logger.debug "Using config with .cluster.insecure_skip_tls_verify"
|
28
|
+
|
29
|
+
options[:ssl_verify_peer] = false
|
30
|
+
end
|
31
|
+
|
32
|
+
if path = config.cluster.certificate_authority
|
33
|
+
logger.debug "Using config with .cluster.certificate_authority"
|
34
|
+
|
35
|
+
options[:ssl_ca_file] = path
|
36
|
+
end
|
37
|
+
|
38
|
+
if data = config.cluster.certificate_authority_data
|
39
|
+
logger.debug "Using config with .cluster.certificate_authority_data"
|
40
|
+
|
41
|
+
ssl_cert_store = options[:ssl_cert_store] = OpenSSL::X509::Store.new
|
42
|
+
ssl_cert_store.add_cert(OpenSSL::X509::Certificate.new(Base64.decode64(data)))
|
43
|
+
end
|
44
|
+
|
45
|
+
if (cert = config.user.client_certificate) && (key = config.user.client_key)
|
46
|
+
logger.debug "Using config with .user.client_certificate/client_key"
|
47
|
+
|
48
|
+
options[:client_cert] = cert
|
49
|
+
options[:client_key] = key
|
50
|
+
end
|
51
|
+
|
52
|
+
if (cert_data = config.user.client_certificate_data) && (key_data = config.user.client_key_data)
|
53
|
+
logger.debug "Using config with .user.client_certificate_data/client_key_data"
|
54
|
+
|
55
|
+
options[:client_cert_data] = Base64.decode64(cert_data)
|
56
|
+
options[:client_key_data] = Base64.decode64(key_data)
|
57
|
+
end
|
58
|
+
|
59
|
+
logger.info "Using config with server=#{config.cluster.server}"
|
60
|
+
|
61
|
+
new(config.cluster.server, **options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# In-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
|
65
|
+
#
|
66
|
+
# @return [K8s::Transport]
|
67
|
+
def self.in_cluster_config
|
68
|
+
host = ENV['KUBERNETES_SERVICE_HOST']
|
69
|
+
port = ENV['KUBERNETES_SERVICE_PORT_HTTPS']
|
70
|
+
|
71
|
+
new("https://#{host}:#{port}",
|
72
|
+
ssl_verify_peer: true,
|
73
|
+
ssl_ca_file: '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt',
|
74
|
+
auth_token: File.read('/var/run/secrets/kubernetes.io/serviceaccount/token'),
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
attr_reader :server, :options
|
79
|
+
|
80
|
+
# @param server [String] URL with protocol://host:port - any /path is ignored
|
81
|
+
def initialize(server, auth_token: nil, **options)
|
82
|
+
@server = server
|
83
|
+
@auth_token = auth_token
|
84
|
+
@options = options
|
85
|
+
|
86
|
+
logger! progname: @server
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Excon::Connection]
|
90
|
+
def excon
|
91
|
+
@excon ||= Excon.new(@server,
|
92
|
+
persistent: true,
|
93
|
+
middlewares: EXCON_MIDDLEWARES,
|
94
|
+
headers: REQUEST_HEADERS,
|
95
|
+
**@options
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String]
|
100
|
+
def path(*path)
|
101
|
+
File.join('/', *path)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [Hash]
|
105
|
+
def request_options(request_object: nil, **options)
|
106
|
+
options[:headers] ||= {}
|
107
|
+
|
108
|
+
if @auth_token
|
109
|
+
options[:headers]['Authorization'] = "Bearer #{@auth_token}"
|
110
|
+
end
|
111
|
+
|
112
|
+
if request_object
|
113
|
+
options[:headers]['Content-Type'] = 'application/json'
|
114
|
+
options[:body] = request_object.to_json
|
115
|
+
end
|
116
|
+
|
117
|
+
options
|
118
|
+
end
|
119
|
+
|
120
|
+
def format_request(options)
|
121
|
+
method = options[:method]
|
122
|
+
path = options[:path]
|
123
|
+
body = nil
|
124
|
+
|
125
|
+
if options[:query]
|
126
|
+
path += Excon::Utils.query_string(options)
|
127
|
+
end
|
128
|
+
if obj = options[:request_object]
|
129
|
+
body = "<#{obj.class.name}>"
|
130
|
+
end
|
131
|
+
|
132
|
+
[method, path, body].compact.join " "
|
133
|
+
end
|
134
|
+
|
135
|
+
# @raise [K8s::Error]
|
136
|
+
# @raise [Excon::Error] TODO: wrap
|
137
|
+
# @return [response_class, Hash]
|
138
|
+
def parse_response(response, request_options, response_class: nil)
|
139
|
+
method = request_options[:method]
|
140
|
+
path = request_options[:path]
|
141
|
+
content_type, = response.headers['Content-Type'].split(';')
|
142
|
+
|
143
|
+
case content_type
|
144
|
+
when 'application/json'
|
145
|
+
response_data = JSON.parse(response.body)
|
146
|
+
|
147
|
+
when 'text/plain'
|
148
|
+
response_data = response.body # XXX: broken if status 2xx
|
149
|
+
else
|
150
|
+
raise K8s::Error::API.new(method, path, response.status, "Invalid response Content-Type: #{response.headers['Content-Type']}")
|
151
|
+
end
|
152
|
+
|
153
|
+
if response.status.between? 200, 299
|
154
|
+
unless response_data.is_a? Hash
|
155
|
+
raise K8s::Error::API.new(method, path, response.status, "Invalid JSON response: #{response_data.inspect}")
|
156
|
+
end
|
157
|
+
|
158
|
+
if response_class
|
159
|
+
return response_class.from_json(response_data)
|
160
|
+
else
|
161
|
+
return response_data # Hash
|
162
|
+
end
|
163
|
+
else
|
164
|
+
error_class = K8s::Error::HTTP_STATUS_ERRORS[response.status] || K8s::Error::API
|
165
|
+
|
166
|
+
if response_data.is_a?(Hash) && response_data['kind'] == 'Status'
|
167
|
+
status = K8s::API::MetaV1::Status.new(response_data)
|
168
|
+
|
169
|
+
raise error_class.new(method, path, response.status, response.reason_phrase, status)
|
170
|
+
else
|
171
|
+
raise error_class.new(method, path, response.status, response.reason_phrase)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def request(response_class: nil, **options)
|
177
|
+
excon_options = request_options(**options)
|
178
|
+
|
179
|
+
start = Time.now
|
180
|
+
response = excon.request(**excon_options)
|
181
|
+
t = Time.now - start
|
182
|
+
|
183
|
+
obj = parse_response(response, options,
|
184
|
+
response_class: response_class,
|
185
|
+
)
|
186
|
+
rescue K8s::Error::API => exc
|
187
|
+
logger.warn { "#{format_request(options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s"}
|
188
|
+
logger.debug { "Request: #{excon_options[:body]}"} if excon_options[:body]
|
189
|
+
logger.debug { "Response: #{response.body}"}
|
190
|
+
raise
|
191
|
+
else
|
192
|
+
logger.info { "#{format_request(options)} => HTTP #{response.status}: <#{obj.class}> in #{'%.3f' % t}s"}
|
193
|
+
logger.debug { "Request: #{excon_options[:body]}"} if excon_options[:body]
|
194
|
+
logger.debug { "Response: #{response.body}"}
|
195
|
+
return obj
|
196
|
+
end
|
197
|
+
|
198
|
+
# @param options [Array<Hash>]
|
199
|
+
# @param skip_missing [Boolean] return nil for 404
|
200
|
+
# @return [Array<response_class, Hash, nil>]
|
201
|
+
def requests(*options, response_class: nil, skip_missing: false)
|
202
|
+
return [] if options.empty? # excon chokes
|
203
|
+
|
204
|
+
start = Time.now
|
205
|
+
responses = excon.requests(
|
206
|
+
options.map{|options| request_options(**options)}
|
207
|
+
)
|
208
|
+
t = Time.now - start
|
209
|
+
|
210
|
+
objects = responses.zip(options).map{|response, request_options|
|
211
|
+
begin
|
212
|
+
parse_response(response, request_options,
|
213
|
+
response_class: request_options[:response_class] || response_class,
|
214
|
+
)
|
215
|
+
rescue K8s::Error::NotFound
|
216
|
+
if skip_missing
|
217
|
+
nil
|
218
|
+
else
|
219
|
+
raise
|
220
|
+
end
|
221
|
+
end
|
222
|
+
}
|
223
|
+
rescue K8s::Error => exc
|
224
|
+
logger.warn { "[#{options.map{|o| format_request(o)}.join ', '}] => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s"}
|
225
|
+
raise
|
226
|
+
else
|
227
|
+
logger.info { "[#{options.map{|o| format_request(o)}.join ', '}] => HTTP [#{responses.map{|r| r.status}.join ', '}] in #{'%.3f' % t}s" }
|
228
|
+
return objects
|
229
|
+
end
|
230
|
+
|
231
|
+
# @param *path [String]
|
232
|
+
def get(*path, **options)
|
233
|
+
request(
|
234
|
+
method: 'GET',
|
235
|
+
path: self.path(*path),
|
236
|
+
**options,
|
237
|
+
)
|
238
|
+
end
|
239
|
+
|
240
|
+
# @param *paths [String]
|
241
|
+
def gets(*paths, response_class: nil, **options)
|
242
|
+
requests(*paths.map{|path| {
|
243
|
+
method: 'GET',
|
244
|
+
path: self.path(path),
|
245
|
+
**options,
|
246
|
+
} },
|
247
|
+
response_class: response_class,
|
248
|
+
)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
metadata
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: k8s-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kontena, Inc.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: excon
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.62.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.62.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: dry-struct
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.5.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: deep_merge
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.2.1
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.2.1
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: recursive-open-struct
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.1.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.1.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.16'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.16'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '3.7'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '3.7'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: webmock
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.4.2
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 3.4.2
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- info@kontena.io
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rspec"
|
134
|
+
- ".travis.yml"
|
135
|
+
- Dockerfile
|
136
|
+
- Gemfile
|
137
|
+
- LICENSE
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
140
|
+
- bin/k8s-client
|
141
|
+
- docker-compose.yaml
|
142
|
+
- k8s-client.gemspec
|
143
|
+
- lib/k8s-client.rb
|
144
|
+
- lib/k8s/api.rb
|
145
|
+
- lib/k8s/api/metav1.rb
|
146
|
+
- lib/k8s/api/metav1/api_group.rb
|
147
|
+
- lib/k8s/api/metav1/api_resource.rb
|
148
|
+
- lib/k8s/api/metav1/list.rb
|
149
|
+
- lib/k8s/api/metav1/object.rb
|
150
|
+
- lib/k8s/api/metav1/status.rb
|
151
|
+
- lib/k8s/api/version.rb
|
152
|
+
- lib/k8s/api_client.rb
|
153
|
+
- lib/k8s/client.rb
|
154
|
+
- lib/k8s/client/version.rb
|
155
|
+
- lib/k8s/config.rb
|
156
|
+
- lib/k8s/error.rb
|
157
|
+
- lib/k8s/logging.rb
|
158
|
+
- lib/k8s/resource.rb
|
159
|
+
- lib/k8s/resource_client.rb
|
160
|
+
- lib/k8s/stack.rb
|
161
|
+
- lib/k8s/transport.rb
|
162
|
+
homepage: https://github.com/kontena/k8s-client
|
163
|
+
licenses:
|
164
|
+
- Apache-2.0
|
165
|
+
metadata: {}
|
166
|
+
post_install_message:
|
167
|
+
rdoc_options: []
|
168
|
+
require_paths:
|
169
|
+
- lib
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - "~>"
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '2.4'
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
requirements: []
|
181
|
+
rubyforge_project:
|
182
|
+
rubygems_version: 2.6.14
|
183
|
+
signing_key:
|
184
|
+
specification_version: 4
|
185
|
+
summary: Kubernetes client library
|
186
|
+
test_files: []
|