k8s-client-renewed 0.10.5.pre.1

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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ class Client
5
+ # Updated on releases using semver.
6
+ VERSION = "0.10.5-1"
7
+ end
8
+ end
data/lib/k8s/config.rb ADDED
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'recursive-open-struct'
4
+ require 'base64'
5
+ require 'yaml'
6
+ require 'time'
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
+ # @see https://godoc.org/k8s.io/client-go/tools/clientcmd/api/v1#Config
14
+ class Config < RecursiveOpenStruct
15
+ using K8s::Util::HashBackport if RUBY_VERSION < "2.5"
16
+
17
+ module KeyTransformations
18
+ def initialize(hash = self.class.defaults, args = {})
19
+ super(hash.to_h.transform_keys { |k| k.to_s.tr('-', '_').to_sym }, args.merge(recurse_over_arrays: true))
20
+ end
21
+
22
+ def to_h
23
+ super.transform_keys { |k| k.to_s.tr('_', '-').to_sym }
24
+ end
25
+ end
26
+
27
+ class Child < RecursiveOpenStruct
28
+ include KeyTransformations
29
+ end
30
+
31
+ include KeyTransformations
32
+
33
+ def self.defaults
34
+ {
35
+ :apiVersion => 'v1',
36
+ :clusters=> [],
37
+ :contexts => [],
38
+ :current_context => nil,
39
+ :kind => 'Config',
40
+ :preferences => {},
41
+ :users => []
42
+ }
43
+ end
44
+
45
+ # Loads a configuration from a YAML file
46
+ #
47
+ # @param path [String]
48
+ # @return [K8s::Config]
49
+ def self.load_file(path)
50
+ new(YAML.safe_load(File.read(File.expand_path(path)), [Time, DateTime, Date], [], true))
51
+ end
52
+
53
+ # Loads configuration files listed in KUBE_CONFIG environment variable and
54
+ # merge using the configuration merge rules, @see K8s::Config.merge
55
+ #
56
+ # @param kubeconfig [String] by default read from ENV['KUBECONFIG']
57
+ def self.from_kubeconfig_env(kubeconfig = nil)
58
+ kubeconfig ||= ENV.fetch('KUBECONFIG', '')
59
+ raise ArgumentError, "KUBECONFIG not set" if kubeconfig.empty?
60
+
61
+ paths = kubeconfig.split(/(?!\\):/)
62
+
63
+ paths.inject(load_file(paths.shift)) do |memo, other_cfg|
64
+ memo.merge(load_file(other_cfg))
65
+ end
66
+ end
67
+
68
+ # Build a minimal configuration from at least a server address, server certificate authority data and an access token.
69
+ #
70
+ # @param server [String] kubernetes server address
71
+ # @param ca [String] server certificate authority data (base64 encoded)
72
+ # @param token [String] access token
73
+ # @param cluster_name [String] cluster name
74
+ # @param user [String] user name
75
+ # @param context [String] context name
76
+ # @param options [Hash] (see #initialize)
77
+ def self.build(server:, ca:, auth_token:, cluster_name: 'kubernetes', user: 'k8s-client', context: 'k8s-client', **options)
78
+ new(
79
+ {
80
+ clusters: [{ name: cluster_name, cluster: { server: server, certificate_authority_data: ca } }],
81
+ users: [{ name: user, user: { token: auth_token } }],
82
+ contexts: [{ name: context, context: { cluster: cluster_name, user: user } }],
83
+ current_context: context
84
+ }.merge(options)
85
+ )
86
+ end
87
+
88
+ # Merges configuration according to the rules specified in
89
+ # https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files
90
+ #
91
+ # @param other [Hash, K8s::Config]
92
+ # @return [K8s::Config]
93
+ def merge(other)
94
+ old_attributes = to_h
95
+ other_attributes = other.is_a?(Hash) ? other : other.to_h
96
+
97
+ old_attributes.merge!(other_attributes) do |key, old_value, new_value|
98
+ case key
99
+ when :clusters, :contexts, :users
100
+ old_value + new_value.reject do |new_mapping|
101
+ old_value.any? { |old_mapping| old_mapping[:name] == new_mapping[:name] }
102
+ end
103
+ else
104
+ case old_value
105
+ when Array
106
+ (old_value + new_value).uniq
107
+ when Hash
108
+ old_value.merge(new_value) do |_key, inner_old_value, inner_new_value|
109
+ inner_old_value.nil? ? inner_new_value : inner_old_value
110
+ end
111
+ when NilClass
112
+ new_value
113
+ else
114
+ old_value
115
+ end
116
+ end
117
+ end
118
+
119
+ self.class.new(old_attributes)
120
+ end
121
+
122
+ # @param name [String]
123
+ # @raise [K8s::Error::Configuration]
124
+ # @return [K8s::Config::Child]
125
+ def context(name = current_context)
126
+ return nil if name.nil?
127
+
128
+ contexts.find { |context| context.name == name }&.context || raise(K8s::Error::Configuration, "context not found: #{name.inspect}")
129
+ end
130
+
131
+ # @param name [String]
132
+ # @raise [K8s::Error::Configuration]
133
+ # @return [K8s::Config::Child]
134
+ def cluster(name = context&.cluster)
135
+ return nil if name.nil?
136
+
137
+ clusters.find { |cluster| cluster.name == name }&.cluster || raise(K8s::Error::Configuration, "cluster not found: #{name.inspect}")
138
+ end
139
+
140
+ # @param name [String]
141
+ # @raise [K8s::Error::Configuration]
142
+ # @return [K8s::Config::User]
143
+ def user(name = context&.user)
144
+ return nil if name.nil?
145
+
146
+ users.find { |user| user.name == name }&.user || raise(K8s::Error::Configuration, "user not found: #{name.inspect}")
147
+ end
148
+
149
+ private
150
+
151
+ # Patch the RecursiveOpenStruct#recurse_over_array method to use a different child class
152
+ def recurse_over_array(array)
153
+ array.each_with_index do |a, i|
154
+ if a.is_a?(Hash)
155
+ array[i] = Child.new(
156
+ a,
157
+ recurse_over_arrays: true,
158
+ mutate_input_hash: true,
159
+ preserve_original_keys: @preserve_original_keys
160
+ )
161
+ elsif a.is_a?(Array)
162
+ array[i] = recurse_over_array(a)
163
+ end
164
+ end
165
+ array
166
+ end
167
+ end
168
+ 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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module K8s
6
+ module JSONParser
7
+ # Standard library JSON -based JSON-parsing backend
8
+ class Default
9
+ # A simple buffer that accepts data through the `<<` method and yields its buffer when a
10
+ # newline character is encountered.
11
+ class LineBufferedIO
12
+ def initialize(&block)
13
+ @buffer = +''
14
+ @parser = block
15
+ end
16
+
17
+ # @param data [String] partial JSON content
18
+ def <<(data)
19
+ chunks = data.rpartition("\n")
20
+ tail = chunks.pop
21
+ @buffer << chunks.join
22
+
23
+ if @buffer.include?("\n")
24
+ @buffer.each_line do |line|
25
+ @parser.call(line) unless line.strip.empty?
26
+ end
27
+
28
+ @buffer.clear
29
+ end
30
+
31
+ @buffer << tail
32
+ end
33
+ end
34
+
35
+ # @param input [String] JSON content
36
+ # @return [Hash,Array,String,Integer,Float,DateTime] the parsed result
37
+ def self.parse(input)
38
+ JSON.parse(input)
39
+ end
40
+
41
+ # Instantiate a streaming parser.
42
+ #
43
+ # @example
44
+ # result = []
45
+ # parser = K8s::JSONParser::Default.new do |obj|
46
+ # result << obj
47
+ # end
48
+ # parser << '{"hello":"world"\n}'
49
+ # parser << '{"hello":"world"\n}'
50
+ # puts result.inspect
51
+ # # outputs:
52
+ # # [{ 'hello' => 'world' }, { 'hello' => 'world' }]
53
+ def initialize(&block)
54
+ @parser = LineBufferedIO.new do |data|
55
+ block.call(JSON.parse(data))
56
+ end
57
+ end
58
+
59
+ def <<(data)
60
+ @parser << data
61
+ end
62
+ end
63
+
64
+ def self.backend
65
+ @backend ||= Default
66
+ end
67
+
68
+ # @param parser_class [Class] set a different JSON parser backend
69
+ def self.backend=(parser_class)
70
+ @backend = parser_class
71
+ end
72
+
73
+ # @param input [String] JSON content
74
+ # @return [Object] parse outcome
75
+ def self.parse(input)
76
+ backend.parse(input)
77
+ end
78
+
79
+
80
+ # Instantiate a streaming JSON parser.
81
+ #
82
+ # @example
83
+ # result = []
84
+ # parser = K8s::JSONParser.new do |obj|
85
+ # result << obj
86
+ # end
87
+ # parser << '{"hello":"world"\n}'
88
+ # parser << '{"hello":"world"\n}'
89
+ # puts result.inspect
90
+ # # outputs:
91
+ # # [{ 'hello' => 'world' }, { 'hello' => 'world' }]
92
+ # @return [K8s::JSONParser::Default,K8s::JSONParser::Yajl]
93
+ def self.new(&block)
94
+ backend.new(&block)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yajl'
4
+
5
+ module K8s
6
+ module JSONParser
7
+ # Yajl JSON-parsing backend, with the native extension it's faster than the pure-ruby standard library JSON-parser.
8
+ #
9
+ # To use install, add `gem 'yajl-ruby'` to your gemfile and require this class: `require 'k8s/json_parser/yajl'`
10
+ class Yajl
11
+ # @param input [String] JSON content
12
+ # @return [Hash,Array,String,Integer,Float,DateTime] the parsed result
13
+ def self.parse(input)
14
+ ::Yajl::Parser.parse(input)
15
+ end
16
+
17
+ # Instantiate a streaming parser.
18
+ #
19
+ # @example
20
+ # result = []
21
+ # parser = K8s::JSONParser::Yajl.new do |obj|
22
+ # result << obj
23
+ # end
24
+ # parser << '{"hello":"world"\n}'
25
+ # parser << '{"hello":"world"\n}'
26
+ # puts result.inspect
27
+ # # outputs:
28
+ # # [{ 'hello' => 'world' }, { 'hello' => 'world' }]
29
+ def initialize(&block)
30
+ @parser = ::Yajl::Parser.new.tap do |parser|
31
+ parser.on_parse_complete = block
32
+ end
33
+ end
34
+
35
+ # The block passed to #new will be executed for each complete document
36
+ # @param data [String] partial JSON content
37
+ def <<(data)
38
+ @parser << data
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ K8s::JSONParser.backend = K8s::JSONParser::Yajl
@@ -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