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.
@@ -0,0 +1,5 @@
1
+ module K8s
2
+ class Client
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,100 @@
1
+ require 'dry-struct'
2
+ require 'dry-types'
3
+ require 'yaml'
4
+
5
+ module K8s
6
+ class ConfigStruct < Dry::Struct
7
+ # convert string keys to symbols
8
+ # normalize foo-bar to foo_bar
9
+ transform_keys do |key|
10
+ case key
11
+ when String
12
+ key.gsub('-', '_').to_sym
13
+ else
14
+ key
15
+ end
16
+ end
17
+ end
18
+
19
+ # @see https://godoc.org/k8s.io/client-go/tools/clientcmd/api/v1#Config
20
+ class Config < ConfigStruct
21
+ class Types
22
+ include Dry::Types.module
23
+ end
24
+
25
+ class Cluster < ConfigStruct
26
+ attribute :server, Types::String
27
+ attribute :insecure_skip_tls_verify, Types::Bool.optional.default(nil)
28
+ attribute :certificate_authority, Types::String.optional.default(nil)
29
+ attribute :certificate_authority_data, Types::String.optional.default(nil)
30
+ attribute :extensions, Types::Strict::Array.optional.default(nil)
31
+ end
32
+ class NamedCluster < ConfigStruct
33
+ attribute :name, Types::String
34
+ attribute :cluster, Cluster
35
+ end
36
+
37
+ class User < ConfigStruct
38
+ attribute :client_certificate, Types::String.optional.default(nil)
39
+ attribute :client_certificate_data, Types::String.optional.default(nil)
40
+ attribute :client_key, Types::String.optional.default(nil)
41
+ attribute :client_key_data, Types::String.optional.default(nil)
42
+ attribute :token, Types::String.optional.default(nil)
43
+ attribute :tokenFile, Types::String.optional.default(nil)
44
+ attribute :as, Types::String.optional.default(nil)
45
+ attribute :as_groups, Types::Array.of(Types::String).optional.default(nil)
46
+ attribute :as_user_extra, Types::Hash.optional.default(nil)
47
+ attribute :username, Types::String.optional.default(nil)
48
+ attribute :password, Types::String.optional.default(nil)
49
+ attribute :auth_provider, Types::Strict::Hash.optional.default(nil)
50
+ attribute :exec, Types::Strict::Hash.optional.default(nil)
51
+ attribute :extensions, Types::Strict::Array.optional.default(nil)
52
+ end
53
+ class NamedUser < ConfigStruct
54
+ attribute :name, Types::String
55
+ attribute :user, User
56
+ end
57
+
58
+ class Context < ConfigStruct
59
+ attribute :cluster, Types::Strict::String
60
+ attribute :user, Types::Strict::String
61
+ attribute :namespace, Types::Strict::String.optional.default(nil)
62
+ attribute :extensions, Types::Strict::Array.optional.default(nil)
63
+ end
64
+ class NamedContext < ConfigStruct
65
+ attribute :name, Types::String
66
+ attribute :context, Context
67
+ end
68
+
69
+ attribute :kind, Types::Strict::String.optional.default(nil)
70
+ attribute :apiVersion, Types::Strict::String.optional.default(nil)
71
+ attribute :preferences, Types::Strict::Hash.optional
72
+ attribute :clusters, Types::Strict::Array.of(NamedCluster)
73
+ attribute :users, Types::Strict::Array.of(NamedUser)
74
+ attribute :contexts, Types::Strict::Array.of(NamedContext)
75
+ attribute :current_context, Types::Strict::String
76
+ attribute :extensions, Types::Strict::Array.optional.default(nil)
77
+
78
+ # @param path [String]
79
+ # @return [K8s::Config]
80
+ def self.load_file(path)
81
+ return new(YAML.load_file(path))
82
+ end
83
+
84
+ # TODO: raise error if not found
85
+ # @return [K8s::Config::Context]
86
+ def context(name = current_context)
87
+ contexts.find{|context| context.name == name}.context
88
+ end
89
+
90
+ # @return [K8s::Config::Cluster]
91
+ def cluster(name = context.cluster)
92
+ clusters.find{|cluster| cluster.name == name}.cluster
93
+ end
94
+
95
+ # @return [K8s::Config::User]
96
+ def user(name = context.user)
97
+ users.find{|user| user.name == name}.user
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,47 @@
1
+ require 'forwardable'
2
+
3
+ module K8s
4
+ class Error < StandardError
5
+ class API < Error
6
+ extend Forwardable
7
+
8
+ attr_reader :method, :path, :code, :reason, :status
9
+
10
+ # @param method [Integer] HTTP request method
11
+ # @param path [Integer] HTTP request path
12
+ # @param code [Integer] HTTP response code
13
+ # @param reason [String] HTTP response reason
14
+ # @param status [K8s::API::MetaV1::Status]
15
+ def initialize(method, path, code, reason, status = nil)
16
+ @method = method
17
+ @path = path
18
+ @code = code
19
+ @reason = reason
20
+ @status = status
21
+
22
+ if status
23
+ super("#{@method} #{@path} => HTTP #{@code} #{@reason}: #{@status.message}")
24
+ else
25
+ super("#{@method} #{@path} => HTTP #{@code} #{@reason}")
26
+ end
27
+ end
28
+ end
29
+
30
+ HTTP_STATUS_ERRORS = {}
31
+
32
+ def self.define_status_error(code, name)
33
+ HTTP_STATUS_ERRORS[code] = self.const_set(name, Class.new(API))
34
+ end
35
+
36
+ define_status_error 400, :BadRequest
37
+ define_status_error 401, :Unauthorized
38
+ define_status_error 403, :Forbidden
39
+ define_status_error 404, :NotFound
40
+ define_status_error 405, :MethodNotAllowed
41
+ define_status_error 409, :Conflict # XXX: also AlreadyExists?
42
+ define_status_error 422, :Invalid
43
+ define_status_error 429, :Timeout
44
+ define_status_error 500, :InternalError
45
+ define_status_error 504, :ServerTimeout
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ require 'logger'
2
+
3
+ module K8s
4
+ module Logging
5
+ LOG_TARGET = STDERR
6
+ LOG_LEVEL = Logger::WARN
7
+
8
+ module ModuleMethods
9
+ # global log_level shared across all including classes
10
+ def log_level
11
+ @log_level
12
+ end
13
+ def log_level=(level)
14
+ @log_level = level
15
+ end
16
+ def debug!
17
+ self.log_level = Logger::DEBUG
18
+ end
19
+ def verbose!
20
+ self.log_level = Logger::INFO
21
+ end
22
+ def quiet!
23
+ self.log_level = Logger::ERROR
24
+ end
25
+ end
26
+
27
+ extend ModuleMethods # global @log_level
28
+
29
+ module ClassMethods
30
+ def logger(target: LOG_TARGET, level: nil)
31
+ @logger ||= Logger.new(target).tap do |logger|
32
+ logger.progname = self.name
33
+ logger.level = level || self.log_level || K8s::Logging.log_level || LOG_LEVEL
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.included(base)
39
+ base.extend(ModuleMethods) # per-class @log_level
40
+ base.extend(ClassMethods)
41
+ end
42
+
43
+ # Use per-instance logger
44
+ def logger!(progname: self.class.name, target: LOG_TARGET, level: nil, debug: false)
45
+ @logger = Logger.new(target).tap do |logger|
46
+ level = Logger::DEBUG if debug
47
+
48
+ logger.progname = "#{self.class.name}<#{progname}>"
49
+ logger.level = level || self.class.log_level || K8s::Logging.log_level || LOG_LEVEL
50
+ end
51
+ end
52
+
53
+ # @return [Logger]
54
+ def logger
55
+ @logger || self.class.logger
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,67 @@
1
+ require 'deep_merge'
2
+ require 'recursive-open-struct'
3
+
4
+ module K8s
5
+ # generic untyped resource
6
+ class Resource < RecursiveOpenStruct
7
+ extend Forwardable
8
+ include Comparable
9
+
10
+ # @param data [Hash]
11
+ # @return [self]
12
+ def self.from_json(data)
13
+ return new(data)
14
+ end
15
+
16
+ # @param filename [String] file path
17
+ # @return [K8s::Resource]
18
+ def self.from_file(filename)
19
+ new(YAML.load_file(filename))
20
+ end
21
+
22
+ # @param filename [String] file path
23
+ # @return [Array<K8s::Resource>]
24
+ def self.from_files(path)
25
+ stat = File.stat(path)
26
+
27
+ if stat.directory?
28
+ Dir.glob("#{path}/*.{yml,yaml}").map{|path| self.load_files(path) }.flatten
29
+ else
30
+ ::YAML.load_stream(File.read(path), path).map{|doc| new(doc) }
31
+ end
32
+ end
33
+
34
+ # @param attrs [Hash]
35
+ def initialize(hash, recurse_over_arrays: true, **options)
36
+ super(hash,
37
+ recurse_over_arrays: recurse_over_arrays,
38
+ **options
39
+ )
40
+ end
41
+
42
+ # @return [String]
43
+ def to_json(**options)
44
+ to_hash.to_json(**options)
45
+ end
46
+
47
+ # @param other [K8s::Resource]
48
+ # @return [Boolean]
49
+ def <=>(other)
50
+ to_hash <=> other.to_hash
51
+ end
52
+
53
+ # merge in fields
54
+ #
55
+ # @param attrs [Hash, K8s::Resource]
56
+ # @return [K8s::Resource]
57
+ def merge(attrs)
58
+ # deep clone of attrs
59
+ h = to_hash
60
+
61
+ # merge in-place
62
+ h.deep_merge!(attrs.to_hash, overwrite_arrays: true)
63
+
64
+ self.class.new(h)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,259 @@
1
+ module K8s
2
+ class ResourceClient
3
+ module Utils
4
+ # @param selector [nil, String, Hash{String => String}]
5
+ # @return [nil, String]
6
+ def selector_query(selector)
7
+ case selector
8
+ when nil
9
+ nil
10
+ when String
11
+ selector
12
+ when Hash
13
+ selector.map{|k, v| "#{k}=#{v}"}.join ','
14
+ else
15
+ fail "Invalid selector type. #{selector.inspect}"
16
+ end
17
+ end
18
+
19
+ # @param options [Hash]
20
+ # @return [Hash, nil]
21
+ def make_query(options)
22
+ query = options.compact
23
+
24
+ return nil if query.empty?
25
+
26
+ query
27
+ end
28
+ end
29
+
30
+ include Utils
31
+ extend Utils
32
+
33
+ # Pipeline list requests for multiple resource types.
34
+ #
35
+ # Returns flattened array with mixed resource kinds.
36
+ #
37
+ # @param resources [Array<K8s::ResourceClient>]
38
+ # @param transport [K8s::Transport]
39
+ # @param namespace [String, nil]
40
+ # @param labelSelector [nil, String, Hash{String => String}]
41
+ # @param fieldSelector [nil, String, Hash{String => String}]
42
+ # @return [Array<K8s::Resource>]
43
+ def self.list(resources, transport, namespace: nil, labelSelector: nil, fieldSelector: nil)
44
+ api_paths = resources.map{|resource| resource.path(namespace: namespace) }
45
+ api_lists = transport.gets(*api_paths,
46
+ response_class: K8s::API::MetaV1::List,
47
+ query: make_query(
48
+ 'labelSelector' => selector_query(labelSelector),
49
+ 'fieldSelector' => selector_query(fieldSelector),
50
+ ),
51
+ )
52
+
53
+ resources.zip(api_lists).map {|resource, api_list| resource.process_list(api_list) }.flatten
54
+ end
55
+
56
+ # @param transport [K8s::Transport]
57
+ # @param api_client [K8s::APIClient]
58
+ # @param api_resource [K8s::API::MetaV1::APIResource]
59
+ # @param namespace [String]
60
+ def initialize(transport, api_client, api_resource, namespace: nil, resource_class: K8s::Resource)
61
+ @transport = transport
62
+ @api_client = api_client
63
+ @api_resource = api_resource
64
+ @namespace = namespace
65
+ @resource_class = resource_class
66
+
67
+ if @api_resource.name.include? '/'
68
+ @resource, @subresource = @api_resource.name.split('/', 2)
69
+ else
70
+ @resource = @api_resource.name
71
+ @subresource = nil
72
+ end
73
+
74
+ fail "Resource #{api_resource.name} is not namespaced" if namespace unless api_resource.namespaced
75
+ end
76
+
77
+ # @return [String]
78
+ def api_version
79
+ @api_client.api_version
80
+ end
81
+
82
+ # @return [String] resource or resource/subresource
83
+ def name
84
+ @api_resource.name
85
+ end
86
+
87
+ # @return [String, nil]
88
+ def namespace
89
+ @namespace
90
+ end
91
+
92
+ # @return [String]
93
+ def resource
94
+ @resource
95
+ end
96
+
97
+ # @return [String, nil]
98
+ def subresource
99
+ @subresource
100
+ end
101
+
102
+ # @return [String]
103
+ def kind
104
+ @api_resource.kind
105
+ end
106
+
107
+ # @return [class] K8s::Resource
108
+ def resource_class
109
+ @resource_class
110
+ end
111
+
112
+ # @return [Bool]
113
+ def namespaced?
114
+ !!@api_resource.namespaced
115
+ end
116
+
117
+ # @return [String]
118
+ def path(name = nil, subresource: @subresource, namespace: @namespace)
119
+ namespace_part = namespace ? ['namespaces', namespace] : []
120
+
121
+ if name && subresource
122
+ @api_client.path(*namespace_part, @resource, name, subresource)
123
+ elsif name
124
+ @api_client.path(*namespace_part, @resource, name)
125
+ else
126
+ @api_client.path(*namespace_part, @resource)
127
+ end
128
+ end
129
+
130
+ # @return [Bool]
131
+ def create?
132
+ @api_resource.verbs.include? 'create'
133
+ end
134
+
135
+ # @param resource [resource_class] with metadata.namespace and metadata.name set
136
+ # @return [resource_class]
137
+ def create_resource(resource)
138
+ @transport.request(
139
+ method: 'POST',
140
+ path: self.path(namespace: resource.metadata.namespace),
141
+ request_object: resource,
142
+ response_class: @resource_class,
143
+ )
144
+ end
145
+
146
+ # @return [Bool]
147
+ def get?
148
+ @api_resource.verbs.include? 'get'
149
+ end
150
+
151
+ # @return [resource_class]
152
+ def get(name, namespace: @namespace)
153
+ @transport.request(
154
+ method: 'GET',
155
+ path: self.path(name, namespace: namespace),
156
+ response_class: @resource_class,
157
+ )
158
+ end
159
+
160
+ # @param resource [resource_class]
161
+ # @return [resource_class]
162
+ def get_resource(resource)
163
+ @transport.request(
164
+ method: 'GET',
165
+ path: self.path(resource.metadata.name, namespace: resource.metadata.namespace),
166
+ response_class: @resource_class,
167
+ )
168
+ end
169
+
170
+ # @return [Bool]
171
+ def list?
172
+ @api_resource.verbs.include? 'list'
173
+ end
174
+
175
+ # @param list [K8s::API::MetaV1::List]
176
+ # @return [Array<resource_class>]
177
+ def process_list(list)
178
+ list.items.map {|item|
179
+ # list items omit kind/apiVersion
180
+ @resource_class.new(item.merge('apiVersion' => list.apiVersion, 'kind' => @api_resource.kind))
181
+ }
182
+ end
183
+
184
+ # @param labelSelector [nil, String, Hash{String => String}]
185
+ # @param fieldSelector [nil, String, Hash{String => String}]
186
+ # @return [Array<resource_class>]
187
+ def list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
188
+ list = @transport.request(
189
+ method: 'GET',
190
+ path: self.path(namespace: namespace),
191
+ response_class: K8s::API::MetaV1::List,
192
+ query: make_query(
193
+ 'labelSelector' => selector_query(labelSelector),
194
+ 'fieldSelector' => selector_query(fieldSelector),
195
+ ),
196
+ )
197
+ process_list(list)
198
+ end
199
+
200
+ # @return [Bool]
201
+ def update?
202
+ @api_resource.verbs.include? 'update'
203
+ end
204
+
205
+ # @param resource [resource_class] with metadata.resourceVersion set
206
+ # @return [resource_class]
207
+ def update_resource(resource)
208
+ @transport.request(
209
+ method: 'PUT',
210
+ path: self.path(resource.metadata.name, namespace: resource.metadata.namespace),
211
+ request_object: resource,
212
+ response_class: @resource_class,
213
+ )
214
+ end
215
+
216
+ # @return [Bool]
217
+ def delete?
218
+ @api_resource.verbs.include? 'delete'
219
+ end
220
+
221
+ # @param name [String]
222
+ # @param namespace [String]
223
+ # @return [K8s::API::MetaV1::Status]
224
+ def delete(name, namespace: @namespace, propagationPolicy: nil)
225
+ @transport.request(
226
+ method: 'DELETE',
227
+ path: self.path(name, namespace: namespace),
228
+ query: make_query(
229
+ 'propagationPolicy' => propagationPolicy,
230
+ ),
231
+ response_class: @resource_class, # XXX: documented as returning Status
232
+ )
233
+ end
234
+
235
+ # @param namespace [String]
236
+ # @param labelSelector [nil, String, Hash{String => String}]
237
+ # @param fieldSelector [nil, String, Hash{String => String}]
238
+ # @return [K8s::API::MetaV1::Status]
239
+ def delete_collection(namespace: @namespace, labelSelector: nil, fieldSelector: nil, propagationPolicy: nil)
240
+ list = @transport.request(
241
+ method: 'DELETE',
242
+ path: self.path(namespace: namespace),
243
+ query: make_query(
244
+ 'labelSelector' => selector_query(labelSelector),
245
+ 'fieldSelector' => selector_query(fieldSelector),
246
+ 'propagationPolicy' => propagationPolicy,
247
+ ),
248
+ response_class: K8s::API::MetaV1::List, # XXX: documented as returning Status
249
+ )
250
+ process_list(list)
251
+ end
252
+
253
+ # @param resource [resource_class] with metadata
254
+ # @return [K8s::API::MetaV1::Status]
255
+ def delete_resource(resource, **options)
256
+ delete(resource.metadata.name, namespace: resource.metadata.namespace, **options)
257
+ end
258
+ end
259
+ end