k8s-client-renewed 0.10.5.pre.1

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