k8s-ruby2 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
data/lib/k8s/client.rb ADDED
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'yajl'
6
+ require 'monitor'
7
+ require 'uri'
8
+
9
+ require 'k8s/util'
10
+
11
+ require 'k8s/api/metav1'
12
+ require 'k8s/api/version'
13
+ require 'k8s/config'
14
+ require 'k8s/logging'
15
+ require 'k8s/api_client'
16
+ require "k8s/error"
17
+ require 'k8s/resource'
18
+ require 'k8s/resource_client'
19
+ require 'k8s/stack'
20
+ require 'k8s/transport'
21
+
22
+ module K8s
23
+ # @param server [String] http/s URL
24
+ # @param options [Hash] @see Transport.new
25
+ # @return [K8s::Client]
26
+ def self.client(server, **options)
27
+ Client.new(Transport.new(server, **options))
28
+ end
29
+
30
+ # Top-level client wrapper.
31
+ # Uses a {Transport} instance to talk to the kube API.
32
+ # Offers access to {APIClient} and {ResourceClient} instances.
33
+ class Client
34
+ # @param config [Phraos::Kube::Config]
35
+ # @param namespace [String] @see #initialize
36
+ # @param options [Hash] @see Transport.config
37
+ # @return [K8s::Client]
38
+ def self.config(config, namespace: nil, **options)
39
+ new(
40
+ Transport.config(config, **options),
41
+ namespace: namespace
42
+ )
43
+ end
44
+
45
+ # An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
46
+ # @see K8s::Transport#in_cluster_config
47
+ #
48
+ # @param namespace [String] default namespace for all operations
49
+ # @param options [Hash] options passed to transport, @see Transport#in_cluster_config
50
+ # @return [K8s::Client]
51
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
52
+ def self.in_cluster_config(namespace: nil, **options)
53
+ new(Transport.in_cluster_config(**options), namespace: namespace)
54
+ end
55
+
56
+ # Attempts to create a K8s::Client instance automatically using environment variables, existing configuration
57
+ # files or in cluster configuration.
58
+ #
59
+ # Look-up order:
60
+ # - KUBE_TOKEN, KUBE_CA, KUBE_SERVER environment variables
61
+ # - KUBECONFIG environment variable
62
+ # - $HOME/.kube/config file
63
+ # - In cluster configuration
64
+ #
65
+ # Will raise when no means of configuration is available
66
+ #
67
+ # @param options [Hash] default namespace for all operations
68
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
69
+ # @return [K8s::Client]
70
+ def self.autoconfig(namespace: nil, **options)
71
+ if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
72
+ unless Base64.decode64(ENV['KUBE_CA']).match?(/CERTIFICATE/)
73
+ raise ArgumentError, 'KUBE_CA does not seem to be base64 encoded'
74
+ end
75
+
76
+ begin
77
+ token = options[:auth_token] || Base64.strict_decode64(ENV['KUBE_TOKEN'])
78
+ rescue ArgumentError
79
+ raise ArgumentError, 'KUBE_TOKEN does not seem to be base64 encoded'
80
+ end
81
+
82
+ configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: token)
83
+ elsif !ENV['KUBECONFIG'].to_s.empty?
84
+ configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'])
85
+ else
86
+ found_config = [
87
+ File.join(Dir.home, '.kube', 'config'),
88
+ '/etc/kubernetes/admin.conf',
89
+ '/etc/kubernetes/kubelet.conf'
90
+ ].find { |f| File.exist?(f) && File.readable?(f) }
91
+
92
+ configuration = K8s::Config.load_file(found_config) if found_config
93
+ end
94
+
95
+ if configuration
96
+ config(configuration, namespace: namespace, **options)
97
+ else
98
+ in_cluster_config(namespace: namespace, **options)
99
+ end
100
+ end
101
+
102
+ include MonitorMixin
103
+
104
+ attr_reader :transport
105
+
106
+ # @param transport [K8s::Transport]
107
+ # @param namespace [String, nil] default namespace for all operations
108
+ def initialize(transport, namespace: nil)
109
+ @transport = transport
110
+ @namespace = namespace
111
+
112
+ @api_clients = {}
113
+ super()
114
+ end
115
+
116
+ # @raise [K8s::Error]
117
+ # @return [K8s::API::Version]
118
+ def version
119
+ @version ||= @transport.version
120
+ end
121
+
122
+ # @param api_version [String] "group/version" or "version" (core)
123
+ # @return [APIClient]
124
+ def api(api_version = 'v1')
125
+ @api_clients[api_version] ||= APIClient.new(@transport, api_version)
126
+ end
127
+
128
+ # Force-update /apis cache.
129
+ # Required if creating new CRDs/apiservices.
130
+ #
131
+ # @return [Array<String>]
132
+ def api_groups!
133
+ synchronize do
134
+ @api_groups = @transport.get(
135
+ '/apis',
136
+ response_class: K8s::API::MetaV1::APIGroupList
137
+ ).groups.flat_map{ |api_group| api_group.versions.map(&:groupVersion) }
138
+
139
+ @api_clients.clear
140
+ end
141
+
142
+ @api_groups
143
+ end
144
+
145
+ # Cached /apis preferred group apiVersions
146
+ # @return [Array<String>]
147
+ def api_groups
148
+ @api_groups || api_groups!
149
+ end
150
+
151
+ # @param api_versions [Array<String>] defaults to all APIs
152
+ # @param prefetch_resources [Boolean] prefetch any missing api_resources for each api_version
153
+ # @param skip_missing [Boolean] return APIClient without api_resources? if 404
154
+ # @return [Array<APIClient>]
155
+ def apis(api_versions = nil, prefetch_resources: false, skip_missing: false)
156
+ api_versions ||= ['v1'] + api_groups
157
+
158
+ if prefetch_resources
159
+ # api groups that are missing their api_resources
160
+ api_paths = api_versions
161
+ .uniq
162
+ .reject{ |api_version| api(api_version).api_resources? }
163
+ .map{ |api_version| APIClient.path(api_version) }
164
+
165
+ # load into APIClient.api_resources=
166
+ begin
167
+ @transport.gets(*api_paths, response_class: K8s::API::MetaV1::APIResourceList, skip_missing: skip_missing).each do |api_resource_list|
168
+ api(api_resource_list.groupVersion).api_resources = api_resource_list.resources if api_resource_list
169
+ end
170
+ rescue K8s::Error::NotFound, K8s::Error::ServiceUnavailable
171
+ # kubernetes api is in unstable state
172
+ # because this is only performance optimization, better to skip prefetch and move on
173
+ end
174
+ end
175
+
176
+ api_versions.map{ |api_version| api(api_version) }
177
+ end
178
+
179
+ # @param namespace [String, nil]
180
+ # @return [Array<K8s::ResourceClient>]
181
+ def resources(namespace: nil)
182
+ apis(prefetch_resources: true).map { |api|
183
+ begin
184
+ api.resources(namespace: namespace)
185
+ rescue K8s::Error::ServiceUnavailable, K8s::Error::NotFound
186
+ []
187
+ end
188
+ }.flatten
189
+ end
190
+
191
+ # Pipeline list requests for multiple resource types.
192
+ #
193
+ # Returns flattened array with mixed resource kinds.
194
+ #
195
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
196
+ # @param options @see K8s::ResourceClient#list
197
+ # @return [Array<K8s::Resource>]
198
+ def list_resources(resources = nil, **options)
199
+ cached_clients = @api_clients.size.positive?
200
+ resources ||= self.resources.select(&:list?)
201
+
202
+ begin
203
+ ResourceClient.list(resources, @transport, **options)
204
+ rescue K8s::Error::NotFound
205
+ raise unless cached_clients
206
+
207
+ cached_clients = false
208
+ api_groups!
209
+ retry
210
+ end
211
+ end
212
+
213
+ # @param resource [K8s::Resource]
214
+ # @param namespace [String, nil] default if resource is missing namespace
215
+ # @raise [K8s::Error::NotFound] API Group does not exist
216
+ # @raise [K8s::Error::UndefinedResource]
217
+ # @return [K8s::ResourceClient]
218
+ def client_for_resource(resource, namespace: nil)
219
+ api(resource.apiVersion).client_for_resource(resource, namespace: namespace)
220
+ end
221
+
222
+ # @param resource [K8s::Resource]
223
+ # @return [K8s::Resource]
224
+ def create_resource(resource)
225
+ client_for_resource(resource).create_resource(resource)
226
+ end
227
+
228
+ # @param resource [K8s::Resource]
229
+ # @return [K8s::Resource]
230
+ def get_resource(resource)
231
+ client_for_resource(resource).get_resource(resource)
232
+ end
233
+
234
+ # Returns nils for any resources that do not exist.
235
+ # This includes custom resources that were not yet defined.
236
+ #
237
+ # @param resources [Array<K8s::Resource>]
238
+ # @return [Array<K8s::Resource, nil>] matching resources array 1:1
239
+ def get_resources(resources)
240
+ # prefetch api resources, skip missing APIs
241
+ resource_apis = apis(resources.map(&:apiVersion), prefetch_resources: true, skip_missing: true)
242
+
243
+ # map each resource to excon request options, or nil if resource is not (yet) defined
244
+ requests = resources.zip(resource_apis).map{ |resource, api_client|
245
+ next nil unless api_client.api_resources?
246
+
247
+ resource_client = api_client.client_for_resource(resource)
248
+
249
+ {
250
+ method: 'GET',
251
+ path: resource_client.path(resource.metadata.name, namespace: resource.metadata.namespace),
252
+ response_class: resource_client.resource_class
253
+ }
254
+ }
255
+
256
+ # map non-nil requests to response objects, or nil for nil request options
257
+ Util.compact_map(requests) { |reqs|
258
+ @transport.requests(*reqs, skip_missing: true)
259
+ }
260
+ end
261
+
262
+ # @param resource [K8s::Resource]
263
+ # @return [K8s::Resource]
264
+ def update_resource(resource)
265
+ client_for_resource(resource).update_resource(resource)
266
+ end
267
+
268
+ # @param resource [K8s::Resource]
269
+ # @param options [Hash]
270
+ # @see ResourceClient#delete for options
271
+ # @return [K8s::Resource]
272
+ def delete_resource(resource, **options)
273
+ client_for_resource(resource).delete_resource(resource, **options)
274
+ end
275
+
276
+ # @param resource [K8s::Resource]
277
+ # @param attrs [Hash]
278
+ # @return [K8s::Client]
279
+ def patch_resource(resource, attrs)
280
+ client_for_resource(resource).json_patch(resource.metadata.name, attrs)
281
+ end
282
+ end
283
+ end
data/lib/k8s/config.rb ADDED
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-struct'
4
+ require 'dry-types'
5
+ require 'base64'
6
+ require 'yaml'
7
+
8
+ module K8s
9
+ # Common struct type for kubeconfigs:
10
+ #
11
+ # * converts string keys to symbols
12
+ # * normalizes foo-bar to foo_bar
13
+ class ConfigStruct < Dry::Struct
14
+ transform_keys do |key|
15
+ case key
16
+ when String
17
+ key.tr('-', '_').to_sym
18
+ else
19
+ key
20
+ end
21
+ end
22
+ end
23
+
24
+ # @see https://godoc.org/k8s.io/client-go/tools/clientcmd/api/v1#Config
25
+ class Config < ConfigStruct
26
+ # Common dry-types for config
27
+ class Types
28
+ include Dry::Types()
29
+ end
30
+
31
+ # structured cluster
32
+ class Cluster < ConfigStruct
33
+ attribute :server, Types::String
34
+ attribute :insecure_skip_tls_verify,
35
+ Types::Bool.optional.default(nil, shared: true)
36
+ attribute :certificate_authority,
37
+ Types::String.optional.default(nil, shared: true)
38
+ attribute :certificate_authority_data,
39
+ Types::String.optional.default(nil, shared: true)
40
+ attribute :extensions,
41
+ Types::Strict::Array.optional.default(nil, shared: true)
42
+ end
43
+
44
+ # structured cluster with name
45
+ class NamedCluster < ConfigStruct
46
+ attribute :name, Types::String
47
+ attribute :cluster, Cluster
48
+ end
49
+
50
+ # structured user auth provider
51
+ class UserAuthProvider < ConfigStruct
52
+ attribute :name, Types::String
53
+ attribute :config, Types::Strict::Hash
54
+ end
55
+
56
+ # structured user exec
57
+ class UserExec < ConfigStruct
58
+ attribute :command, Types::String
59
+ attribute :apiVersion, Types::String
60
+ attribute :env, Types::Strict::Array.of(Types::Hash).optional.default(nil)
61
+ attribute :args, Types::Strict::Array.of(Types::String).optional.default(nil, shared: true)
62
+ end
63
+
64
+ # structured user
65
+ class User < ConfigStruct
66
+ attribute :client_certificate, Types::String.optional.default(nil, shared: true)
67
+ attribute :client_certificate_data, Types::String.optional.default(nil, shared: true)
68
+ attribute :client_key, Types::String.optional.default(nil, shared: true)
69
+ attribute :client_key_data, Types::String.optional.default(nil, shared: true)
70
+ attribute :token, Types::String.optional.default(nil, shared: true)
71
+ attribute :tokenFile, Types::String.optional.default(nil, shared: true)
72
+ attribute :as, Types::String.optional.default(nil, shared: true)
73
+ attribute :as_groups, Types::Array.of(Types::String).optional.default(nil, shared: true)
74
+ attribute :as_user_extra, Types::Hash.optional.default(nil, shared: true)
75
+ attribute :username, Types::String.optional.default(nil, shared: true)
76
+ attribute :password, Types::String.optional.default(nil, shared: true)
77
+ attribute :auth_provider, UserAuthProvider.optional.default(nil, shared: true)
78
+ attribute :exec, UserExec.optional.default(nil, shared: true)
79
+ attribute :extensions, Types::Strict::Array.optional.default(nil, shared: true)
80
+ end
81
+
82
+ # structured user with name
83
+ class NamedUser < ConfigStruct
84
+ attribute :name, Types::String
85
+ attribute :user, User
86
+ end
87
+
88
+ # structured context
89
+ #
90
+ # Referrs to other named User/cluster objects within the same config.
91
+ class Context < ConfigStruct
92
+ attribute :cluster, Types::Strict::String
93
+ attribute :user, Types::Strict::String
94
+ attribute :namespace, Types::Strict::String.optional.default(nil, shared: true)
95
+ attribute :extensions, Types::Strict::Array.optional.default(nil, shared: true)
96
+ end
97
+
98
+ # named context
99
+ class NamedContext < ConfigStruct
100
+ attribute :name, Types::String
101
+ attribute :context, Context
102
+ end
103
+
104
+ attribute :kind, Types::Strict::String.optional.default(nil, shared: true)
105
+ attribute :apiVersion, Types::Strict::String.optional.default(nil, shared: true)
106
+ attribute :preferences, Types::Strict::Hash.optional.default(proc { {} }, shared: true)
107
+ attribute :clusters, Types::Strict::Array.of(NamedCluster).optional.default(proc { [] }, shared: true)
108
+ attribute :users, Types::Strict::Array.of(NamedUser).optional.default(proc { [] }, shared: true)
109
+ attribute :contexts, Types::Strict::Array.of(NamedContext).optional.default(proc { [] }, shared: true)
110
+ attribute :current_context, Types::Strict::String.optional.default(nil, shared: true)
111
+ attribute :extensions, Types::Strict::Array.optional.default(proc { [] }, shared: true)
112
+
113
+ # Loads a configuration from a YAML file
114
+ #
115
+ # @param path [String]
116
+ # @return [K8s::Config]
117
+ def self.load_file(path)
118
+ new(YAML.safe_load(File.read(File.expand_path(path)), [Time, DateTime, Date], [], true))
119
+ end
120
+
121
+ # Loads configuration files listed in KUBE_CONFIG environment variable and
122
+ # merge using the configuration merge rules, @see K8s::Config.merge
123
+ #
124
+ # @param kubeconfig [String] by default read from ENV['KUBECONFIG']
125
+ def self.from_kubeconfig_env(kubeconfig = nil)
126
+ kubeconfig ||= ENV.fetch('KUBECONFIG', '')
127
+ raise ArgumentError, "KUBECONFIG not set" if kubeconfig.empty?
128
+
129
+ paths = kubeconfig.split(/(?!\\):/)
130
+
131
+ paths.inject(load_file(paths.shift)) do |memo, other_cfg|
132
+ memo.merge(load_file(other_cfg))
133
+ end
134
+ end
135
+
136
+ # Build a minimal configuration from at least a server address, server certificate authority data and an access token.
137
+ #
138
+ # @param server [String] kubernetes server address
139
+ # @param ca [String] server certificate authority data (base64 encoded)
140
+ # @param token [String] access token
141
+ # @param cluster_name [String] cluster name
142
+ # @param user [String] user name
143
+ # @param context [String] context name
144
+ # @param options [Hash] (see #initialize)
145
+ def self.build(server:, ca:, auth_token:, cluster_name: 'kubernetes', user: 'k8s-client', context: 'k8s-client', **options)
146
+ new(
147
+ {
148
+ clusters: [{ name: cluster_name, cluster: { server: server, certificate_authority_data: ca } }],
149
+ users: [{ name: user, user: { token: auth_token } }],
150
+ contexts: [{ name: context, context: { cluster: cluster_name, user: user } }],
151
+ current_context: context
152
+ }.merge(options)
153
+ )
154
+ end
155
+
156
+ # Merges configuration according to the rules specified in
157
+ # https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files
158
+ #
159
+ # @param other [Hash, K8s::Config]
160
+ # @return [K8s::Config]
161
+ def merge(other)
162
+ old_attributes = attributes
163
+ other_attributes = other.is_a?(Hash) ? other : other.attributes
164
+
165
+ old_attributes.merge!(other_attributes) do |key, old_value, new_value|
166
+ case key
167
+ when :clusters, :contexts, :users
168
+ old_value + new_value.reject do |new_mapping|
169
+ old_value.any? { |old_mapping| old_mapping[:name] == new_mapping[:name] }
170
+ end
171
+ else
172
+ case old_value
173
+ when Array
174
+ (old_value + new_value).uniq
175
+ when Hash
176
+ old_value.merge(new_value) do |_key, inner_old_value, inner_new_value|
177
+ inner_old_value.nil? ? inner_new_value : inner_old_value
178
+ end
179
+ when NilClass
180
+ new_value
181
+ else
182
+ old_value
183
+ end
184
+ end
185
+ end
186
+
187
+ self.class.new(old_attributes)
188
+ end
189
+
190
+ # @param name [String]
191
+ # @raise [K8s::Error::Configuration]
192
+ # @return [K8s::Config::Context]
193
+ def context(name = current_context)
194
+ found = contexts.find{ |context| context.name == name }
195
+ raise K8s::Error::Configuration, "context not found: #{name.inspect}" unless found
196
+
197
+ found.context
198
+ end
199
+
200
+ # @param name [String]
201
+ # @return [K8s::Config::Cluster]
202
+ def cluster(name = context.cluster)
203
+ clusters.find{ |cluster| cluster.name == name }.cluster
204
+ end
205
+
206
+ # @param name [String]
207
+ # @return [K8s::Config::User]
208
+ def user(name = context.user)
209
+ users.find{ |user| user.name == name }.user
210
+ end
211
+ end
212
+ end
data/lib/k8s/error.rb ADDED
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module K8s
6
+ # Top-level class for all errors raised by this gem.
7
+ class Error < StandardError
8
+ # Kube API error, related to a HTTP response with a non-2xx code
9
+ class API < Error
10
+ extend Forwardable
11
+
12
+ attr_reader :method, :path, :code, :reason, :status
13
+
14
+ # @param method [Integer] HTTP request method
15
+ # @param path [Integer] HTTP request path
16
+ # @param code [Integer] HTTP response code
17
+ # @param reason [String] HTTP response reason
18
+ # @param status [K8s::API::MetaV1::Status]
19
+ def initialize(method, path, code, reason, status = nil)
20
+ @method = method
21
+ @path = path
22
+ @code = code
23
+ @reason = reason
24
+ @status = status
25
+
26
+ if status
27
+ super("#{@method} #{@path} => HTTP #{@code} #{@reason}: #{@status.message}")
28
+ else
29
+ super("#{@method} #{@path} => HTTP #{@code} #{@reason}")
30
+ end
31
+ end
32
+ end
33
+
34
+ BadRequest = Class.new(API).freeze
35
+ Unauthorized = Class.new(API).freeze
36
+ Forbidden = Class.new(API).freeze
37
+ NotFound = Class.new(API).freeze
38
+ MethodNotAllowed = Class.new(API).freeze
39
+ Conflict = Class.new(API).freeze # XXX: also AlreadyExists?
40
+ Invalid = Class.new(API).freeze
41
+ Timeout = Class.new(API).freeze
42
+ InternalError = Class.new(API).freeze
43
+ ServiceUnavailable = Class.new(API).freeze
44
+ ServerTimeout = Class.new(API).freeze
45
+
46
+ HTTP_STATUS_ERRORS = {
47
+ 400 => BadRequest,
48
+ 401 => Unauthorized,
49
+ 403 => Forbidden,
50
+ 404 => NotFound,
51
+ 405 => MethodNotAllowed,
52
+ 409 => Conflict,
53
+ 422 => Invalid,
54
+ 429 => Timeout,
55
+ 500 => InternalError,
56
+ 503 => ServiceUnavailable,
57
+ 504 => ServerTimeout
58
+ }.freeze
59
+
60
+ # Attempt to create a ResourceClient for an unknown resource type.
61
+ # The client cannot construct the correct API URL without having the APIResource definition.
62
+ UndefinedResource = Class.new(Error)
63
+
64
+ Configuration = Class.new(Error)
65
+ end
66
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module K8s
6
+ # Logging utilities
7
+ #
8
+ # This provides a per-class Logger that uses the class name as a logging prefix.
9
+ # Instances can optionally also use logger! to define a per-instance Logger using a custom prefix.
10
+ module Logging
11
+ # Default log target
12
+ LOG_TARGET = $stderr
13
+
14
+ # Default log level: show warnings.
15
+ #
16
+ # Use K8s::Logging.quiet! to supress warnings.
17
+ # Note that the K8s::Transport defaults to quiet!
18
+ LOG_LEVEL = Logger::WARN
19
+
20
+ # methods defined on both the global K8s::Logging module, as well as class methods on each class including K8s::Logging
21
+ module ModuleMethods
22
+ # global log_level shared across all including classes
23
+ # @return Logger::*
24
+ def log_level
25
+ @log_level
26
+ end
27
+
28
+ # @param level Logger::*
29
+ def log_level=(level)
30
+ @log_level = level
31
+ end
32
+
33
+ # Set log_level to Logger::DEBUG
34
+ def debug!
35
+ self.log_level = Logger::DEBUG
36
+ end
37
+
38
+ # Set log_level to Logger::INFO
39
+ def verbose!
40
+ self.log_level = Logger::INFO
41
+ end
42
+
43
+ # Set log_level to Logger::ERROR, surpressing any warnings logged by default
44
+ def quiet!
45
+ self.log_level = Logger::ERROR
46
+ end
47
+ end
48
+
49
+ extend ModuleMethods # global @log_level
50
+
51
+ # methods defined on each class including K8s::Logging
52
+ module ClassMethods
53
+ # @return [Logger]
54
+ def logger(target: LOG_TARGET, level: nil)
55
+ @logger ||= Logger.new(target).tap do |logger|
56
+ logger.progname = name
57
+ logger.level = level || log_level || K8s::Logging.log_level || LOG_LEVEL
58
+ end
59
+ end
60
+ end
61
+
62
+ # extend class/intance methods for per-class logger
63
+ def self.included(base)
64
+ base.extend(ModuleMethods) # per-class @log_level
65
+ base.extend(ClassMethods)
66
+ end
67
+
68
+ # Use per-instance logger instead of the default per-class logger
69
+ #
70
+ # Sets the instance variable returned by #logger
71
+ #
72
+ # @return [Logger]
73
+ def logger!(progname: self.class.name, target: LOG_TARGET, level: nil, debug: false)
74
+ @logger = Logger.new(target).tap do |logger|
75
+ level = Logger::DEBUG if debug
76
+
77
+ logger.progname = "#{self.class.name}<#{progname}>"
78
+ logger.level = level || self.class.log_level || K8s::Logging.log_level || LOG_LEVEL
79
+ end
80
+ end
81
+
82
+ # @return [Logger]
83
+ def logger
84
+ @logger || self.class.logger
85
+ end
86
+ end
87
+ end