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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.relaxed.yml +176 -0
- data/.rubocop.yml +57 -0
- data/.travis.yml +20 -0
- data/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +233 -0
- data/Rakefile +16 -0
- data/docker-compose.yaml +10 -0
- data/k8s-client.gemspec +39 -0
- data/lib/k8s-client.rb +3 -0
- data/lib/k8s/api_client.rb +115 -0
- data/lib/k8s/client.rb +282 -0
- data/lib/k8s/client/version.rb +8 -0
- data/lib/k8s/config.rb +168 -0
- data/lib/k8s/error.rb +66 -0
- data/lib/k8s/json_parser.rb +97 -0
- data/lib/k8s/json_parser/yajl.rb +44 -0
- data/lib/k8s/logging.rb +87 -0
- data/lib/k8s/resource.rb +111 -0
- data/lib/k8s/resource_client.rb +349 -0
- data/lib/k8s/stack.rb +187 -0
- data/lib/k8s/transport.rb +380 -0
- data/lib/k8s/util.rb +178 -0
- data/lib/k8s/watch_event.rb +12 -0
- metadata +224 -0
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
|
data/lib/k8s/logging.rb
ADDED
@@ -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
|