k8s-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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